251 lines
7.4 KiB
Go
251 lines
7.4 KiB
Go
package main
|
|
|
|
// autotitle_test.go — needsAutoTitle gating, firstUserText extraction,
|
|
// requestTitle against a fake ZAI endpoint, apply path renames + broadcasts.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func userMsgEndFrame(sid string, seq int64, text string) []byte {
|
|
b, _ := json.Marshal(map[string]any{
|
|
"v": 1, "sessionId": sid, "seq": seq, "ts": 1, "type": "message_end",
|
|
"message": map[string]any{"role": "user", "id": "u", "text": text,
|
|
"thinking": nil, "toolCalls": []any{}, "toolCallId": nil},
|
|
})
|
|
return b
|
|
}
|
|
|
|
func TestNeedsAutoTitleGating(t *testing.T) {
|
|
_, _, hub := newTestServerHub(t)
|
|
f := frame{typ: evMessageEnd, sessionID: "s-a", raw: userMsgEndFrame("s-a", 1, "hello")}
|
|
if !hub.needsAutoTitle(f) {
|
|
t.Fatal("first unnamed user message must want a title")
|
|
}
|
|
if hub.needsAutoTitle(f) {
|
|
t.Fatal("second attempt for same session must be gated")
|
|
}
|
|
// assistant frames never trigger
|
|
fa := frame{typ: evMessageEnd, sessionID: "s-b", raw: []byte(`{"message":{"role":"assistant"}}`)}
|
|
if hub.needsAutoTitle(fa) {
|
|
t.Fatal("assistant message must not trigger")
|
|
}
|
|
// named session never triggers (persisted via UpsertSession)
|
|
if err := hub.store.UpsertSession(SessionInfo{ID: "s-c", Name: strPtr("named")}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fc := frame{typ: evMessageEnd, sessionID: "s-c", raw: userMsgEndFrame("s-c", 1, "hi")}
|
|
if hub.needsAutoTitle(fc) {
|
|
t.Fatal("named session must not trigger")
|
|
}
|
|
}
|
|
|
|
func strPtr(s string) *string { return &s }
|
|
|
|
func dummyTitleServer(t *testing.T) string {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"Auto generated title"}}]}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv.URL
|
|
}
|
|
|
|
func TestFirstUserText(t *testing.T) {
|
|
if got := firstUserText(userMsgEndFrame("s", 1, "build me a thing")); got != "build me a thing" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
if got := firstUserText([]byte("garbage")); got != "" {
|
|
t.Fatalf("garbage -> %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRequestTitle(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Messages []struct {
|
|
Content string `json:"content"`
|
|
} `json:"messages"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if len(req.Messages) == 0 || req.Messages[0].Content == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"choices": []any{map[string]any{
|
|
"message": map[string]string{"content": " \"Fix login timeout bug\".\n"},
|
|
}},
|
|
})
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
t.Setenv("LVMH_AUTOTITLE_URL", srv.URL)
|
|
got, err := requestTitle("the login page times out after 5s in production")
|
|
if err != nil {
|
|
t.Fatalf("requestTitle: %v", err)
|
|
}
|
|
if got != "Fix login timeout bug" {
|
|
t.Fatalf("title = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAutoTitleAppliesRename(t *testing.T) {
|
|
_, _, hub := newTestServerHub(t)
|
|
t.Setenv("LVMH_AUTOTITLE_URL", dummyTitleServer(t))
|
|
if err := hub.store.UpsertSession(SessionInfo{ID: "s-t"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hub.autoTitle("s-t", "anything")
|
|
rows, err := hub.store.Sessions()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, row := range rows {
|
|
if row.Info.ID == "s-t" {
|
|
if row.Info.Name == nil || *row.Info.Name == "" {
|
|
t.Fatal("name not applied")
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("session row missing")
|
|
}
|
|
|
|
func TestRequestTitleFailures(t *testing.T) {
|
|
t.Run("http-500", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("LVMH_AUTOTITLE_URL", srv.URL)
|
|
if _, err := requestTitle("x"); err == nil {
|
|
t.Fatal("500 must error")
|
|
}
|
|
})
|
|
t.Run("bad-json", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("not json"))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("LVMH_AUTOTITLE_URL", srv.URL)
|
|
if _, err := requestTitle("x"); err == nil {
|
|
t.Fatal("bad json must error")
|
|
}
|
|
})
|
|
t.Run("empty-choices", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"choices":[]}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("LVMH_AUTOTITLE_URL", srv.URL)
|
|
if _, err := requestTitle("x"); err == nil {
|
|
t.Fatal("empty choices must error")
|
|
}
|
|
})
|
|
t.Run("whitespace-only-title", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":" *** "}}]}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("LVMH_AUTOTITLE_URL", srv.URL)
|
|
if _, err := requestTitle("x"); err == nil {
|
|
t.Fatal("whitespace-only title must error")
|
|
}
|
|
})
|
|
t.Run("long-title-clamped", func(t *testing.T) {
|
|
long := make([]byte, 200)
|
|
for i := range long {
|
|
long[i] = 'a'
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"` + string(long) + `"}}]}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("LVMH_AUTOTITLE_URL", srv.URL)
|
|
got, err := requestTitle("x")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 60 {
|
|
t.Fatalf("clamp = %d", len(got))
|
|
}
|
|
})
|
|
t.Run("multiline-title-first-line", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"First line\nsecond"}}]}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("LVMH_AUTOTITLE_URL", srv.URL)
|
|
got, err := requestTitle("x")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "First line" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAutoTitleRequestShape(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Model string `json:"model"`
|
|
MaxTok int `json:"max_tokens"`
|
|
Thinking map[string]any `json:"thinking"`
|
|
Messages []struct {
|
|
Content string `json:"content"`
|
|
} `json:"messages"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
if req.Model == "" || req.MaxTok == 0 || req.Thinking == nil {
|
|
t.Errorf("request shape wrong: %+v", req)
|
|
}
|
|
if len(req.Messages) != 1 || !contains(req.Messages[0].Content, autoTitlePromptTail[:20]) {
|
|
t.Errorf("messages wrong: %+v", req.Messages)
|
|
}
|
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok title"}}]}`))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
t.Setenv("LVMH_AUTOTITLE_URL", srv.URL)
|
|
if _, err := requestTitle("the thing"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func contains(s, sub string) bool {
|
|
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0)
|
|
}
|
|
|
|
func indexOf(s, sub string) int {
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func TestAutoTitleClosedStoreFails(t *testing.T) {
|
|
_, store, hub := newTestServerHub(t)
|
|
if err := hub.store.UpsertSession(SessionInfo{ID: "s-x"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = store.Close()
|
|
t.Setenv("LVMH_AUTOTITLE_URL", dummyTitleServer(t))
|
|
// must not panic; logs the failure
|
|
hub.autoTitle("s-x", "text")
|
|
}
|
|
|
|
func TestAutoTitleNoKey(t *testing.T) {
|
|
// no ZAI key in env -> requestTitle errors before any HTTP call
|
|
t.Setenv(envProviderAPIKey, "")
|
|
t.Setenv("LVMH_AUTOTITLE_URL", "http://127.0.0.1:1")
|
|
if _, err := requestTitle("x"); err == nil {
|
|
t.Fatal("missing key must error")
|
|
}
|
|
}
|