@@ -55,10 +55,12 @@ pub(crate) fn extract_title(messages: &[ChatMessage]) -> String {
5555 . iter ( )
5656 . find ( |m| matches ! ( m. role, MessageRole :: User ) )
5757 . map ( |m| {
58- // Take first line or first 60 characters
58+ // Take first line or first 60 characters (char-boundary safe)
5959 let first_line = m. content . lines ( ) . next ( ) . unwrap_or ( "Untitled Session" ) ;
60- if first_line. len ( ) > 60 {
61- format ! ( "{}..." , & first_line[ ..60 ] )
60+ let char_count = first_line. chars ( ) . count ( ) ;
61+ if char_count > 60 {
62+ let truncated: String = first_line. chars ( ) . take ( 60 ) . collect ( ) ;
63+ format ! ( "{}..." , truncated)
6264 } else {
6365 first_line. to_string ( )
6466 }
@@ -70,3 +72,115 @@ pub(crate) fn extract_title(messages: &[ChatMessage]) -> String {
7072pub ( crate ) fn format_datetime ( dt : & DateTime < Utc > ) -> String {
7173 dt. format ( "%Y-%m-%d %H:%M:%S UTC" ) . to_string ( )
7274}
75+
76+ #[ cfg( test) ]
77+ mod tests {
78+ use super :: * ;
79+ use crate :: providers:: base:: MessageMetadata ;
80+
81+ fn create_test_message ( content : & str , role : MessageRole ) -> ChatMessage {
82+ ChatMessage {
83+ id : "test-id" . to_string ( ) ,
84+ role,
85+ content : content. to_string ( ) ,
86+ timestamp : Utc :: now ( ) ,
87+ metadata : MessageMetadata :: default ( ) ,
88+ }
89+ }
90+
91+ #[ test]
92+ fn test_extract_title_short_english ( ) {
93+ let messages = vec ! [ create_test_message( "Hello world" , MessageRole :: User ) ] ;
94+ let title = extract_title ( & messages) ;
95+ assert_eq ! ( title, "Hello world" ) ;
96+ }
97+
98+ #[ test]
99+ fn test_extract_title_long_english ( ) {
100+ let long_text =
101+ "This is a very long message that exceeds sixty characters and should be truncated" ;
102+ let messages = vec ! [ create_test_message( long_text, MessageRole :: User ) ] ;
103+ let title = extract_title ( & messages) ;
104+ assert ! ( title. ends_with( "..." ) ) ;
105+ assert ! ( title. len( ) <= 63 ) ; // 60 chars + "..."
106+ }
107+
108+ #[ test]
109+ fn test_extract_title_short_chinese ( ) {
110+ let messages = vec ! [ create_test_message( "你好世界" , MessageRole :: User ) ] ;
111+ let title = extract_title ( & messages) ;
112+ assert_eq ! ( title, "你好世界" ) ;
113+ }
114+
115+ #[ test]
116+ fn test_extract_title_long_chinese ( ) {
117+ let long_chinese =
118+ "把 pg_stateful.yaml 改写为 docker compose 可以运行的yaml,输出到 docker-compose.yaml" ;
119+ let messages = vec ! [ create_test_message( long_chinese, MessageRole :: User ) ] ;
120+ // This should not panic
121+ let title = extract_title ( & messages) ;
122+ assert ! ( title. ends_with( "..." ) ) ;
123+ }
124+
125+ #[ test]
126+ fn test_extract_title_mixed_long ( ) {
127+ let mixed = "这是一个包含English和中文的very long message that should be truncated properly without panic" ;
128+ let messages = vec ! [ create_test_message( mixed, MessageRole :: User ) ] ;
129+ let title = extract_title ( & messages) ;
130+ assert ! ( title. ends_with( "..." ) ) ;
131+ }
132+
133+ #[ test]
134+ fn test_extract_title_multiline ( ) {
135+ let multiline = "First line\n Second line\n Third line" ;
136+ let messages = vec ! [ create_test_message( multiline, MessageRole :: User ) ] ;
137+ let title = extract_title ( & messages) ;
138+ assert_eq ! ( title, "First line" ) ;
139+ }
140+
141+ #[ test]
142+ fn test_extract_title_empty_messages ( ) {
143+ let messages: Vec < ChatMessage > = vec ! [ ] ;
144+ let title = extract_title ( & messages) ;
145+ assert_eq ! ( title, "Untitled Session" ) ;
146+ }
147+
148+ #[ test]
149+ fn test_extract_title_no_user_messages ( ) {
150+ let messages = vec ! [
151+ create_test_message( "Assistant response" , MessageRole :: Assistant ) ,
152+ create_test_message( "System message" , MessageRole :: System ) ,
153+ ] ;
154+ let title = extract_title ( & messages) ;
155+ assert_eq ! ( title, "Untitled Session" ) ;
156+ }
157+
158+ #[ test]
159+ fn test_extract_title_exactly_60_chars ( ) {
160+ let exactly_60 = "a" . repeat ( 60 ) ;
161+ let messages = vec ! [ create_test_message( & exactly_60, MessageRole :: User ) ] ;
162+ let title = extract_title ( & messages) ;
163+ assert_eq ! ( title, exactly_60) ;
164+ assert ! ( !title. ends_with( "..." ) ) ;
165+ }
166+
167+ #[ test]
168+ fn test_extract_title_with_emoji ( ) {
169+ let with_emoji = "Hello 👋 this is a message with emoji 🎉 that might be long enough to truncate properly" ;
170+ let messages = vec ! [ create_test_message( with_emoji, MessageRole :: User ) ] ;
171+ let title = extract_title ( & messages) ;
172+ // Should not panic on emoji boundaries
173+ assert ! ( title. len( ) > 0 ) ;
174+ }
175+
176+ #[ test]
177+ fn test_extract_title_finds_first_user_message ( ) {
178+ let messages = vec ! [
179+ create_test_message( "System init" , MessageRole :: System ) ,
180+ create_test_message( "First user message" , MessageRole :: User ) ,
181+ create_test_message( "Second user message" , MessageRole :: User ) ,
182+ ] ;
183+ let title = extract_title ( & messages) ;
184+ assert_eq ! ( title, "First user message" ) ;
185+ }
186+ }
0 commit comments