@@ -215,10 +215,15 @@ vi.mock("./slash-commands.runtime.js", () => {
215215 if ( params . command ?. key === "reportexternal" ) {
216216 return {
217217 arg : { name : "period" , description : "period" } ,
218- choices : Array . from ( { length : 140 } , ( _v , i ) => ( {
219- value : `period-${ i + 1 } ` ,
220- label : `Period ${ i + 1 } ` ,
221- } ) ) ,
218+ choices : [
219+ ...Array . from ( { length : 140 } , ( _v , i ) => ( {
220+ value : `period-${ i + 1 } ` ,
221+ label : `Period ${ i + 1 } ` ,
222+ } ) ) ,
223+ // Label whose emoji surrogate pair straddles the 75-char plain_text
224+ // limit, to cover surrogate-safe truncation in served options.
225+ { value : "emoji-overflow" , label : `${ "a" . repeat ( 74 ) } 😀 emojioverflow` } ,
226+ ] ,
222227 } ;
223228 }
224229 if ( params . command ?. key === "unsafeconfirm" ) {
@@ -872,6 +877,37 @@ describe("Slack native command argument menus", () => {
872877 expect ( optionTexts . join ( "\n" ) ) . toContain ( "Period 12" ) ;
873878 } ) ;
874879
880+ it ( "truncates served option labels on a surrogate boundary" , async ( ) => {
881+ const { blockId } = await runCommandAndResolveActionsBlock ( reportExternalHandler ) ;
882+ expect ( blockId ) . toContain ( "openclaw_cmdarg_ext:" ) ;
883+
884+ const ackOptions = vi . fn ( ) . mockResolvedValue ( undefined ) ;
885+ await argMenuOptionsHandler ( {
886+ ack : ackOptions ,
887+ body : {
888+ user : { id : "U1" } ,
889+ value : "emojioverflow" ,
890+ actions : [ { block_id : blockId } ] ,
891+ } ,
892+ } ) ;
893+
894+ const optionsPayload = firstCallPayload ( ackOptions , "options ack" ) as {
895+ options ?: Array < { text ?: { text ?: string } ; value ?: string } > ;
896+ } ;
897+ // The "emojioverflow" query matches only the long emoji label, so exactly one
898+ // option is served.
899+ const served = optionsPayload . options ?? [ ] ;
900+ expect ( served ) . toHaveLength ( 1 ) ;
901+ const text = served [ 0 ] ?. text ?. text ?? "" ;
902+ // Plain_text option labels are capped at 75 chars and must not end on a lone
903+ // surrogate half, which Slack rejects. The label was long enough to truncate.
904+ expect ( text . length ) . toBeGreaterThan ( 0 ) ;
905+ expect ( text . length ) . toBeLessThanOrEqual ( 75 ) ;
906+ expect (
907+ / [ \uD800 - \uDBFF ] (? ! [ \uDC00 - \uDFFF ] ) | (?< ! [ \uD800 - \uDBFF ] ) [ \uDC00 - \uDFFF ] / . test ( text ) ,
908+ ) . toBe ( false ) ;
909+ } ) ;
910+
875911 it ( "tracks accepted external_select option requests" , async ( ) => {
876912 const trackingHarness = createArgMenusHarness ( ) ;
877913 const trackEvent = vi . fn ( ) ;
0 commit comments