@@ -6,6 +6,24 @@ import {
66} from "./thread-bindings.persona.js" ;
77import type { ThreadBindingRecord } from "./thread-bindings.types.js" ;
88
9+ function hasLoneSurrogate ( value : string ) : boolean {
10+ for ( let index = 0 ; index < value . length ; index += 1 ) {
11+ const code = value . charCodeAt ( index ) ;
12+ if ( code >= 0xd800 && code <= 0xdbff ) {
13+ const next = value . charCodeAt ( index + 1 ) ;
14+ if ( ! ( next >= 0xdc00 && next <= 0xdfff ) ) {
15+ return true ;
16+ }
17+ index += 1 ;
18+ continue ;
19+ }
20+ if ( code >= 0xdc00 && code <= 0xdfff ) {
21+ return true ;
22+ }
23+ }
24+ return false ;
25+ }
26+
927describe ( "thread binding persona" , ( ) => {
1028 it ( "prefers explicit label and prefixes with gear" , ( ) => {
1129 expect ( resolveThreadBindingPersona ( { label : "codex thread" , agentId : "codex" } ) ) . toBe (
@@ -32,4 +50,23 @@ describe("thread binding persona", () => {
3250 } satisfies ThreadBindingRecord ;
3351 expect ( resolveThreadBindingPersonaFromRecord ( record ) ) . toBe ( "⚙️ codex-thread" ) ;
3452 } ) ;
53+ it ( "keeps thread binding webhook personas on a UTF-16 boundary" , ( ) => {
54+ const record = {
55+ accountId : "default" ,
56+ channelId : "parent-1" ,
57+ threadId : "thread-1" ,
58+ targetKind : "acp" ,
59+ targetSessionKey : "agent:codex:acp:session-1" ,
60+ agentId : "codex" ,
61+ boundBy : "system" ,
62+ boundAt : Date . now ( ) ,
63+ lastActivityAt : Date . now ( ) ,
64+ label : `${ "a" . repeat ( 77 ) } \u{1f63e}tail` ,
65+ } satisfies ThreadBindingRecord ;
66+
67+ const persona = resolveThreadBindingPersonaFromRecord ( record ) ;
68+
69+ expect ( persona . length ) . toBeLessThanOrEqual ( 80 ) ;
70+ expect ( hasLoneSurrogate ( persona ) ) . toBe ( false ) ;
71+ } ) ;
3572} ) ;
0 commit comments