@@ -130,6 +130,98 @@ public function test_empty_history() {
130130 $ this ->assertSame ( [], $ this ->service ->prepare_history ( [] ) );
131131 }
132132
133+ /**
134+ * Literal newline escape sequences are converted to real characters.
135+ */
136+ public function test_normalize_answer_text_literal_newline () {
137+ $ this ->assertSame (
138+ "line1 \nline2 " ,
139+ FaqSearchService::normalize_answer_text ( 'line1\nline2 ' )
140+ );
141+ }
142+
143+ /**
144+ * Literal double newlines (paragraph breaks) are converted.
145+ */
146+ public function test_normalize_answer_text_literal_double_newline () {
147+ $ this ->assertSame (
148+ "para1 \n\npara2 " ,
149+ FaqSearchService::normalize_answer_text ( 'para1\n\npara2 ' )
150+ );
151+ }
152+
153+ /**
154+ * A literal CRLF sequence collapses to a single real newline.
155+ */
156+ public function test_normalize_answer_text_literal_crlf () {
157+ $ this ->assertSame (
158+ "line1 \nline2 " ,
159+ FaqSearchService::normalize_answer_text ( 'line1\r\nline2 ' )
160+ );
161+ }
162+
163+ /**
164+ * A literal tab sequence is converted to a real tab.
165+ */
166+ public function test_normalize_answer_text_literal_tab () {
167+ $ this ->assertSame (
168+ "a \tb " ,
169+ FaqSearchService::normalize_answer_text ( 'a\tb ' )
170+ );
171+ }
172+
173+ /**
174+ * Real newline characters are left intact.
175+ */
176+ public function test_normalize_answer_text_real_newline_untouched () {
177+ $ this ->assertSame (
178+ "line1 \nline2 " ,
179+ FaqSearchService::normalize_answer_text ( "line1 \nline2 " )
180+ );
181+ }
182+
183+ /**
184+ * Plain text without escape sequences is returned unchanged.
185+ */
186+ public function test_normalize_answer_text_plain_unchanged () {
187+ $ this ->assertSame (
188+ 'Just a plain answer. ' ,
189+ FaqSearchService::normalize_answer_text ( 'Just a plain answer. ' )
190+ );
191+ }
192+
193+ /**
194+ * A standalone literal carriage return (no following n) becomes one real newline.
195+ */
196+ public function test_normalize_answer_text_literal_lone_cr () {
197+ $ this ->assertSame (
198+ "line1 \nline2 " ,
199+ FaqSearchService::normalize_answer_text ( 'line1\rline2 ' )
200+ );
201+ }
202+
203+ /**
204+ * A real newline and a literal escape sequence in the same string both
205+ * end up as real newlines, with the pre-existing real newline untouched.
206+ */
207+ public function test_normalize_answer_text_mixed_real_and_literal_newline () {
208+ $ this ->assertSame (
209+ "line1 \nline2 \nline3 " ,
210+ FaqSearchService::normalize_answer_text ( "line1 \nline2 " . '\n ' . 'line3 ' )
211+ );
212+ }
213+
214+ /**
215+ * Normalization is idempotent: running it on already-normalized output
216+ * produces the same result as a single pass.
217+ */
218+ public function test_normalize_answer_text_idempotent () {
219+ $ input = 'a\r\nb\tc ' . "\n" . 'd ' ;
220+ $ once = FaqSearchService::normalize_answer_text ( $ input );
221+ $ twice = FaqSearchService::normalize_answer_text ( $ once );
222+ $ this ->assertSame ( $ once , $ twice );
223+ }
224+
133225 /**
134226 * The AI Overview mode defaults to conversation and honors option/filter.
135227 */
0 commit comments