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") } }