1+ // Voice-call tests cover the UTF-16-safe partial-transcript tail slice in
2+ // webhook/realtime-handler.ts (limitPartialUserTranscript). Verifies that
3+ // `sliceUtf16Safe(text, -MAX_PARTIAL_USER_TRANSCRIPT_CHARS)` drops a surrogate
4+ // pair that straddles the tail boundary instead of leaving a lone high-
5+ // surrogate half in the partial transcript fed to the agent.
6+ import { describe , expect , it } from "vitest" ;
7+ import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime" ;
8+
9+ describe ( "voice-call partial-transcript tail UTF-16" , ( ) => {
10+ // Mirrors MAX_PARTIAL_USER_TRANSCRIPT_CHARS in realtime-handler.ts. The
11+ // exact cap is not exposed publicly, but the UTF-16-safe slicing contract
12+ // is the same regardless of the numeric value, so we mirror a representative
13+ // small cap here.
14+ const MAX_PARTIAL_USER_TRANSCRIPT_CHARS = 200 ;
15+ const emoji = "🎉" ;
16+
17+ it ( "sliceUtf16Safe drops a surrogate pair straddling the tail-start boundary" , ( ) => {
18+ // Build a string of length 301 where the emoji sits at positions
19+ // (100, 101). With sliceUtf16Safe(input, -200), `from = 301 - 200 = 101`,
20+ // which is the low surrogate of the emoji, and position 100 is the high
21+ // surrogate. The helper increments `from` past the high surrogate so the
22+ // tail does not start with a lone surrogate half.
23+ const input = "a" . repeat ( 100 ) + emoji + "a" . repeat ( 199 ) ;
24+ const tail = sliceUtf16Safe ( input , - MAX_PARTIAL_USER_TRANSCRIPT_CHARS ) ;
25+ // Tail was supposed to be 200 code units, but the dangling high surrogate
26+ // at position 100 is dropped, so the tail is 199 code units.
27+ expect ( tail . length ) . toBe ( 199 ) ;
28+ expect ( tail . charCodeAt ( 0 ) ) . toBeLessThan ( 0xd800 ) ;
29+ } ) ;
30+
31+ it ( "sliceUtf16Safe is a pass-through for plain ASCII partial transcript" , ( ) => {
32+ const input = "hello world, this is a normal transcript" ;
33+ expect ( sliceUtf16Safe ( input , - MAX_PARTIAL_USER_TRANSCRIPT_CHARS ) ) . toBe ( input ) ;
34+ } ) ;
35+
36+ it ( "sliceUtf16Safe preserves an emoji that sits entirely inside the tail window" , ( ) => {
37+ // Input is exactly 200 chars. The emoji at position 100 is fully inside
38+ // the tail window (no surrogate straddles the boundary).
39+ const input = "a" . repeat ( 100 ) + emoji + "a" . repeat ( 98 ) ;
40+ const tail = sliceUtf16Safe ( input , - MAX_PARTIAL_USER_TRANSCRIPT_CHARS ) ;
41+ expect ( tail . length ) . toBe ( 200 ) ;
42+ expect ( tail . includes ( emoji ) ) . toBe ( true ) ;
43+ } ) ;
44+
45+ it ( "limitPartialUserTranscript-shaped behavior: short body passes through" , ( ) => {
46+ // Mirror the production guard at the top of limitPartialUserTranscript:
47+ // when text.length <= MAX_PARTIAL_USER_TRANSCRIPT_CHARS, the helper is
48+ // not called (the production guard short-circuits first), so sliceUtf16Safe
49+ // here is verifying a no-op for short input.
50+ const input = "short" ;
51+ expect ( sliceUtf16Safe ( input , - MAX_PARTIAL_USER_TRANSCRIPT_CHARS ) ) . toBe ( input ) ;
52+ } ) ;
53+ } ) ;
0 commit comments