@@ -347,26 +347,67 @@ function hasDanglingSurrogate(value: string): boolean {
347347}
348348
349349describe ( "formatSkillExperienceReviewTranscript" , ( ) => {
350+ const EXPERIENCE_REVIEW_MAX_TRANSCRIPT_CHARS = 60_000 ;
351+ // U+1F600 (😀) is a surrogate pair in UTF-16; a raw .slice() can split it.
352+ const EMOJI = "😀" ;
353+ // U+1F99E (🦞) is a surrogate pair in UTF-16.
354+ const LOBSTER = "🦞" ;
355+
350356 it ( "keeps first-message truncation UTF-16 safe at the 6 000-char boundary" , ( ) => {
351- // [user]\n is 7 chars; 5 992 padding chars reach position 5 999 where 😀 starts.
352- // Without UTF-16 safety, .slice(0, 6_000) would split the surrogate pair.
353- const content = "a" . repeat ( 5_992 ) + "😀rest" ;
354- const messages = [ { role : "user" , content } ] ;
355- const transcript = formatSkillExperienceReviewTranscript ( messages ) ;
357+ // renderMessage({role:"user", content: "a"×5_992 + 😀 + "rest"})
358+ // → "[user]\n" + "a"×5_992 + 😀 + "rest" (= 6_005 chars)
359+ // 😀 occupies indices [5_999]=\uD83D (high) and [6_000]=\uDE00 (low).
360+ // Old .slice(0, 6_000) → ends at index 5_999 = lone high surrogate \uD83D.
361+ // A second message pushes full > 60_000, triggering the truncation branch.
362+ const content = "a" . repeat ( 5_992 ) + EMOJI + "rest" ;
363+ const msgs = [
364+ { role : "user" , content } ,
365+ { role : "user" , content : "d" . repeat ( 60_000 ) } ,
366+ ] ;
367+
368+ // Pre-condition: old raw slice on the first rendered message would dangle
369+ const renderHeader = "[user]\n" ;
370+ const rendered0 = `${ renderHeader } ${ content } ` ;
371+ expect ( rendered0 . length ) . toBeGreaterThan ( 6_000 ) ;
372+ const rawSlice = rendered0 . slice ( 0 , 6_000 ) ;
373+ expect ( rawSlice . charCodeAt ( 5_999 ) ) . toBe ( 0xd83d ) ; // lone high surrogate
374+ expect ( hasDanglingSurrogate ( rawSlice ) ) . toBe ( true ) ;
356375
376+ // Production function must NOT produce a dangling surrogate
377+ const transcript = formatSkillExperienceReviewTranscript ( msgs ) ;
357378 expect ( hasDanglingSurrogate ( transcript ) ) . toBe ( false ) ;
358379 } ) ;
359380
360381 it ( "keeps tail-end truncation UTF-16 safe for transcripts exceeding the limit" , ( ) => {
361- // Two long messages to trigger tail truncation (total > 60 000 chars).
362- const content = "b" . repeat ( 35_000 ) ;
363- const messages = [
364- { role : "user" , content : "a" . repeat ( 5_992 ) + "🎉more" } ,
365- { role : "user" , content : content + "🦞after" } ,
382+ // rendered[0]: "[user]\nb"×20_000 → 20_007 plain ASCII chars
383+ // first = truncateUtf16Safe(rendered[0], 6_000) → 6_000 chars
384+ // tailBudget = 60_000 − 6_000 − 80 = 53_920
385+ //
386+ // rendered[1]: "[user]\n" + 🦞 + "z"×53_919 → 53_928 chars
387+ // 🦞 occupies indices 7(\uD83E) and 8(\uDD9E) within rendered[1]
388+ // full = 20_007 + 2 + 53_928 = 73_937
389+ // tailStart = 73_937 − 53_920 = 20_017
390+ //
391+ // full[20_017] = rendered[1][8] = \uDD9E (🦞 low surrogate)
392+ // → old .slice(20_017) starts with a lone low surrogate
393+ const msgs = [
394+ { role : "user" , content : "b" . repeat ( 20_000 ) } ,
395+ { role : "user" , content : LOBSTER + "z" . repeat ( 53_919 ) } ,
366396 ] ;
367- const transcript = formatSkillExperienceReviewTranscript ( messages ) ;
368397
369- expect ( transcript . length ) . toBeLessThan ( 70_000 ) ;
398+ // Pre-condition: old raw slice at tailStart would yield a dangling surrogate
399+ const renderHeader = "[user]\n" ;
400+ const r0 = `${ renderHeader } ${ msgs [ 0 ] ! . content as string } ` ;
401+ const r1 = `${ renderHeader } ${ msgs [ 1 ] ! . content as string } ` ;
402+ const full = `${ r0 } \n\n${ r1 } ` ;
403+ const firstLen = 6_000 ; // truncateUtf16Safe on pure ASCII → exact 6_000
404+ const tailBudget = EXPERIENCE_REVIEW_MAX_TRANSCRIPT_CHARS - firstLen - 80 ;
405+ const tailStart = Math . max ( 0 , full . length - tailBudget ) ;
406+ expect ( full . charCodeAt ( tailStart ) ) . toBe ( 0xdd9e ) ; // lone low surrogate
407+ expect ( hasDanglingSurrogate ( full . slice ( tailStart ) ) ) . toBe ( true ) ;
408+
409+ // Production function must NOT produce a dangling surrogate
410+ const transcript = formatSkillExperienceReviewTranscript ( msgs ) ;
370411 expect ( hasDanglingSurrogate ( transcript ) ) . toBe ( false ) ;
371412 } ) ;
372413} ) ;
0 commit comments