@@ -676,3 +676,57 @@ describe("outbound-media-send UTF-16 truncation cap boundary", () => {
676676 expect ( truncateUtf16Safe ( safePrefix + "🎉 trailing path" , 80 ) ) . toBe ( safePrefix ) ;
677677 } ) ;
678678} ) ;
679+
680+ // Runtime proof for the user-visible error-message template in
681+ // outbound-media-send.ts:460 — evaluates the EXACT production template
682+ // literal against an emoji-boundary input so the BEFORE/AFTER run can be
683+ // pasted into the PR body as evidence that the cap is enforced at the
684+ // surrogate boundary, not just on length.
685+ describe ( "outbound-media-send runtime UTF-16 evidence" , ( ) => {
686+ function hexCodeUnits ( s : string ) : string {
687+ return Array . from ( s )
688+ . map ( ( c ) => "U+" + c . codePointAt ( 0 ) ! . toString ( 16 ) . toUpperCase ( ) . padStart ( 4 , "0" ) )
689+ . join ( " " ) ;
690+ }
691+
692+ it ( "'Failed to download image' template at cap 80 keeps the 79-ASCII prefix and drops the trailing emoji pair" , ( ) => {
693+ // EXACT prod template literal from outbound-media-send.ts:460
694+ // return { channel: "qqbot", error: `Failed to download image: ${truncateUtf16Safe(mediaPath, 80)}` };
695+ const pathPrefix = "/tmp/uploads/" + "x" . repeat ( 66 ) ; // 13 + 66 = 79 ASCII chars total
696+ const mediaPath = pathPrefix + "🎉" + "tail" ; // 79 ASCII + 2 (emoji) + 4 = 85 code units; the emoji straddles the cap-80 boundary at code-unit position 79.
697+
698+ const before = `Failed to download image: ${ mediaPath . slice ( 0 , 80 ) } ` ;
699+ const after = `Failed to download image: ${ truncateUtf16Safe ( mediaPath , 80 ) } ` ;
700+
701+ console . log (
702+ "\n=== PR 1a runtime proof: outbound-media-send Failed-to-download-image error ===" ,
703+ ) ;
704+ console . log ( `input mediaPath (${ mediaPath . length } code units): ${ mediaPath } ` ) ;
705+ console . log ( `input hex: ${ hexCodeUnits ( mediaPath ) } ` ) ;
706+ console . log ( `slice(0, 80) path: ${ mediaPath . slice ( 0 , 80 ) } ` ) ;
707+ console . log ( `slice(0, 80) hex: ${ hexCodeUnits ( mediaPath . slice ( 0 , 80 ) ) } ` ) ;
708+ console . log ( `truncateUtf16Safe: ${ truncateUtf16Safe ( mediaPath , 80 ) } ` ) ;
709+ console . log ( `truncateUtf16Safe hex: ${ hexCodeUnits ( truncateUtf16Safe ( mediaPath , 80 ) ) } ` ) ;
710+ console . log ( `BEFORE full message (${ before . length } code units):` ) ;
711+ console . log ( ` ${ before } ` ) ;
712+ console . log ( ` hex: ${ hexCodeUnits ( before ) } ` ) ;
713+ console . log ( `AFTER full message (${ after . length } code units):` ) ;
714+ console . log ( ` ${ after } ` ) ;
715+ console . log ( ` hex: ${ hexCodeUnits ( after ) } ` ) ;
716+ const beforeHasLone =
717+ / [ \uD800 - \uDBFF ] (? ! [ \uDC00 - \uDFFF ] ) | (?< ! [ \uD800 - \uDBFF ] ) [ \uDC00 - \uDFFF ] / . test ( before ) ;
718+ const afterHasLone =
719+ / [ \uD800 - \uDBFF ] (? ! [ \uDC00 - \uDFFF ] ) | (?< ! [ \uD800 - \uDBFF ] ) [ \uDC00 - \uDFFF ] / . test ( after ) ;
720+ console . log ( `BEFORE contains lone surrogate? ${ beforeHasLone } ` ) ;
721+ console . log ( `AFTER contains lone surrogate? ${ afterHasLone } ` ) ;
722+
723+ // The cap-80 helper preserves the 79-ASCII prefix and drops the
724+ // trailing emoji pair, so the AFTER message ends exactly at the
725+ // prefix without a half-surrogate dangling in the error string.
726+ expect ( after ) . toBe ( `Failed to download image: ${ pathPrefix } ` ) ;
727+ expect ( afterHasLone ) . toBe ( false ) ;
728+ // Sanity: the BEFORE path still emits a lone 0xD83D high surrogate
729+ // in the user-facing error string (the bug being fixed).
730+ expect ( beforeHasLone ) . toBe ( true ) ;
731+ } ) ;
732+ } ) ;
0 commit comments