@@ -346,6 +346,13 @@ function requireRecord(value: unknown, label: string): Record<string, unknown> {
346346 return value as Record < string , unknown > ;
347347}
348348
349+ function hasLoneSurrogate ( value : string ) : boolean {
350+ return Array . from ( value ) . some ( ( char ) => {
351+ const codePoint = char . codePointAt ( 0 ) ?? 0 ;
352+ return codePoint >= 0xd800 && codePoint <= 0xdfff ;
353+ } ) ;
354+ }
355+
349356function expectRecordFields (
350357 actual : Record < string , unknown > ,
351358 expected : Record < string , unknown > ,
@@ -3190,5 +3197,88 @@ describe("registerSlackInteractionEvents", () => {
31903197 expect ( Array . isArray ( payload . inputs ) ? payload . inputs . length : 0 ) . toBeLessThanOrEqual ( 3 ) ;
31913198 expect ( ( payload . inputsOmitted ?? 0 ) >= 1 ) . toBe ( true ) ;
31923199 } ) ;
3200+
3201+ it ( "keeps block action rich text previews UTF-16 safe at the truncation boundary" , async ( ) => {
3202+ enqueueSystemEventMock . mockClear ( ) ;
3203+ const { ctx, getHandler } = createContext ( ) ;
3204+ registerSlackInteractionEvents ( { ctx : ctx as never } ) ;
3205+ const handler = getHandler ( ) ;
3206+
3207+ const boundaryText = `${ "x" . repeat ( 118 ) } 😀y` ;
3208+ const ack = vi . fn ( ) . mockResolvedValue ( undefined ) ;
3209+ await handler ( {
3210+ ack,
3211+ body : {
3212+ user : { id : "U555" } ,
3213+ channel : { id : "C1" } ,
3214+ message : {
3215+ ts : "111.222" ,
3216+ text : "fallback" ,
3217+ blocks : [ { type : "actions" , block_id : "richtext_block" , elements : [ ] } ] ,
3218+ } ,
3219+ } ,
3220+ action : {
3221+ type : "rich_text_input" ,
3222+ action_id : "openclaw:richtext" ,
3223+ block_id : "richtext_block" ,
3224+ rich_text_value : {
3225+ type : "rich_text" ,
3226+ elements : [
3227+ {
3228+ type : "rich_text_section" ,
3229+ elements : [ { type : "text" , text : boundaryText } ] ,
3230+ } ,
3231+ ] ,
3232+ } ,
3233+ } ,
3234+ } as never ) ;
3235+
3236+ expect ( ack ) . toHaveBeenCalled ( ) ;
3237+ const payload = slackInteractionPayload ( ) as { richTextPreview ?: string } ;
3238+ expect ( payload . richTextPreview ) . toBe ( `${ "x" . repeat ( 118 ) } …` ) ;
3239+ expect ( payload . richTextPreview ?. length ) . toBeLessThanOrEqual ( 120 ) ;
3240+ expect ( hasLoneSurrogate ( payload . richTextPreview ?? "" ) ) . toBe ( false ) ;
3241+ expect ( ( ) => encodeURIComponent ( payload . richTextPreview ?? "" ) ) . not . toThrow ( ) ;
3242+ } ) ;
3243+
3244+ it ( "keeps complete emoji in rich text previews when the UTF-16 boundary can include it" , async ( ) => {
3245+ enqueueSystemEventMock . mockClear ( ) ;
3246+ const { ctx, getHandler } = createContext ( ) ;
3247+ registerSlackInteractionEvents ( { ctx : ctx as never } ) ;
3248+ const handler = getHandler ( ) ;
3249+
3250+ const text = `${ "x" . repeat ( 117 ) } 😀yy` ;
3251+ await handler ( {
3252+ ack : vi . fn ( ) . mockResolvedValue ( undefined ) ,
3253+ body : {
3254+ user : { id : "U555" } ,
3255+ channel : { id : "C1" } ,
3256+ message : {
3257+ ts : "111.333" ,
3258+ text : "fallback" ,
3259+ blocks : [ { type : "actions" , block_id : "richtext_block" , elements : [ ] } ] ,
3260+ } ,
3261+ } ,
3262+ action : {
3263+ type : "rich_text_input" ,
3264+ action_id : "openclaw:richtext" ,
3265+ block_id : "richtext_block" ,
3266+ rich_text_value : {
3267+ type : "rich_text" ,
3268+ elements : [
3269+ {
3270+ type : "rich_text_section" ,
3271+ elements : [ { type : "text" , text } ] ,
3272+ } ,
3273+ ] ,
3274+ } ,
3275+ } ,
3276+ } as never ) ;
3277+
3278+ const payload = slackInteractionPayload ( ) as { richTextPreview ?: string } ;
3279+ expect ( payload . richTextPreview ) . toBe ( `${ "x" . repeat ( 117 ) } 😀…` ) ;
3280+ expect ( payload . richTextPreview ?. length ) . toBeLessThanOrEqual ( 120 ) ;
3281+ expect ( hasLoneSurrogate ( payload . richTextPreview ?? "" ) ) . toBe ( false ) ;
3282+ } ) ;
31933283} ) ;
31943284const selectedDateTimeEpoch = 1_771_632_300 ;
0 commit comments