@@ -461,6 +461,76 @@ describe("TwitchClientManager", () => {
461461 } ) ;
462462 } ) ;
463463
464+ describe ( "UTF-16 safe truncation" , ( ) => {
465+ let debugMock : ReturnType < typeof vi . fn > ;
466+
467+ beforeEach ( ( ) => {
468+ debugMock = vi . fn ( ) ;
469+ mockLogger . debug = debugMock as unknown as typeof mockLogger . debug ;
470+ } ) ;
471+
472+ it ( "should not emit lone surrogates in debug log when message has emoji at truncation boundary" , async ( ) => {
473+ await manager . getClient ( testAccount ) ;
474+ const handler = vi . fn ( ) ;
475+ manager . onMessage ( testAccount , handler ) ;
476+
477+ // Build a message where the emoji lands at position 100
478+ const textBeforeEmoji = "x" . repeat ( 99 ) ; // 99 chars
479+ const emoji = "😀" ;
480+ const messageText = `${ textBeforeEmoji } ${ emoji } rest` ;
481+
482+ // Invoke the onMessage callback stored by the mock
483+ const msg = {
484+ id : "test-msg-utf16" ,
485+ userInfo : {
486+ userName : "testuser" ,
487+ displayName : "TestUser" ,
488+ isMod : false ,
489+ isBroadcaster : false ,
490+ isVip : false ,
491+ isSubscriber : false ,
492+ } ,
493+ } ;
494+ messageHandlers [ 0 ] ( "#testchannel" , "testuser" , messageText , msg ) ;
495+
496+ // The debug log preview should not contain a lone high surrogate
497+ const debugCall = debugMock . mock . calls . find ( ( call : string [ ] ) =>
498+ call [ 0 ] ?. startsWith ( "twitch inbound:" ) ,
499+ ) ;
500+ expect ( debugCall ) . toBeDefined ( ) ;
501+ const preview = debugCall ! [ 0 ] as string ;
502+ expect ( preview ) . not . toMatch ( / [ \uD800 - \uDBFF ] (? ! [ \uDC00 - \uDFFF ] ) / ) ;
503+ } ) ;
504+
505+ it ( "should handle plain ASCII text without truncation" , async ( ) => {
506+ await manager . getClient ( testAccount ) ;
507+ const handler = vi . fn ( ) ;
508+ manager . onMessage ( testAccount , handler ) ;
509+
510+ const messageText = "short message without emoji" ;
511+
512+ const msg = {
513+ id : "test-msg-ascii" ,
514+ userInfo : {
515+ userName : "testuser" ,
516+ displayName : "TestUser" ,
517+ isMod : false ,
518+ isBroadcaster : false ,
519+ isVip : false ,
520+ isSubscriber : false ,
521+ } ,
522+ } ;
523+ messageHandlers [ 0 ] ( "#testchannel" , "testuser" , messageText , msg ) ;
524+
525+ const debugCall = debugMock . mock . calls . find ( ( call : string [ ] ) =>
526+ call [ 0 ] ?. startsWith ( "twitch inbound:" ) ,
527+ ) ;
528+ expect ( debugCall ) . toBeDefined ( ) ;
529+ const preview = debugCall ! [ 0 ] as string ;
530+ expect ( preview ) . toContain ( messageText ) ;
531+ } ) ;
532+ } ) ;
533+
464534 describe ( "disconnect" , ( ) => {
465535 it ( "should disconnect a connected client" , async ( ) => {
466536 await manager . getClient ( testAccount ) ;
0 commit comments