@@ -87,6 +87,7 @@ function makeHandler(
8787 config ,
8888 {
8989 processEvent : vi . fn ( ) ,
90+ getCall : vi . fn ( ) ,
9091 getCallByProviderCallId : vi . fn ( ) ,
9192 ...deps ?. manager ,
9293 } as unknown as CallManager ,
@@ -128,6 +129,21 @@ const startRealtimeServer = async (
128129 } ) ;
129130} ;
130131
132+ const startStreamSessionServer = async (
133+ handler : RealtimeCallHandler ,
134+ streamUrl : string ,
135+ ) : Promise < {
136+ url : string ;
137+ close : ( ) => Promise < void > ;
138+ } > => {
139+ return await startUpgradeWsServer ( {
140+ urlPath : new URL ( streamUrl ) . pathname ,
141+ onUpgrade : ( request , socket , head ) => {
142+ handler . handleWebSocketUpgrade ( request , socket , head ) ;
143+ } ,
144+ } ) ;
145+ } ;
146+
131147async function waitForRealtimeTest (
132148 callback : ( ) => void | Promise < void > ,
133149 options : { timeout ?: number ; interval ?: number } = { } ,
@@ -252,6 +268,130 @@ describe("RealtimeCallHandler path routing", () => {
252268 }
253269 } ) ;
254270
271+ it ( "joins Telnyx realtime streams to the token-bound call" , async ( ) => {
272+ const processEvent = vi . fn ( ) ;
273+ const getCall = vi . fn (
274+ ( ) : CallRecord => ( {
275+ callId : "call-1" ,
276+ providerCallId : "v3:call-1" ,
277+ provider : "telnyx" ,
278+ direction : "inbound" ,
279+ state : "answered" ,
280+ from : "+15550001234" ,
281+ to : "+15550009999" ,
282+ startedAt : Date . now ( ) ,
283+ transcript : [ ] ,
284+ processedEventIds : [ ] ,
285+ metadata : { initialMessage : "hello" } ,
286+ } ) ,
287+ ) ;
288+ const createBridge = vi . fn ( ( ) => makeBridge ( ) ) ;
289+ const handler = makeHandler ( undefined , {
290+ manager : {
291+ processEvent,
292+ getCall,
293+ } ,
294+ provider : {
295+ name : "telnyx" ,
296+ } ,
297+ realtimeProvider : makeRealtimeProvider ( createBridge ) ,
298+ } ) ;
299+ handler . setPublicUrl ( "https://public.example/voice/webhook" ) ;
300+ const session = handler . issueStreamSession ( {
301+ providerName : "telnyx" ,
302+ callId : "call-1" ,
303+ from : "+15550001234" ,
304+ to : "+15550009999" ,
305+ direction : "inbound" ,
306+ } ) ;
307+ const server = await startStreamSessionServer ( handler , session . streamUrl ) ;
308+
309+ try {
310+ const ws = await connectWs ( server . url ) ;
311+ try {
312+ ws . send (
313+ JSON . stringify ( {
314+ event : "start" ,
315+ stream_id : "stream-1" ,
316+ start : { call_control_id : "v3:call-1" } ,
317+ } ) ,
318+ ) ;
319+ await waitForRealtimeTest ( ( ) => {
320+ expect ( createBridge ) . toHaveBeenCalled ( ) ;
321+ } ) ;
322+
323+ const eventTypes = processEvent . mock . calls . map (
324+ ( [ event ] ) => ( event as NormalizedEvent ) . type ,
325+ ) ;
326+ expect ( eventTypes ) . toEqual ( [ "call.answered" ] ) ;
327+ expect ( ( processEvent . mock . calls [ 0 ] ?. [ 0 ] as NormalizedEvent | undefined ) ?. callId ) . toBe (
328+ "call-1" ,
329+ ) ;
330+ } finally {
331+ if ( ws . readyState !== WebSocket . CLOSED && ws . readyState !== WebSocket . CLOSING ) {
332+ ws . close ( ) ;
333+ }
334+ }
335+ } finally {
336+ await server . close ( ) ;
337+ }
338+ } ) ;
339+
340+ it ( "rejects Telnyx stream starts that do not match the token-bound call" , async ( ) => {
341+ const processEvent = vi . fn ( ) ;
342+ const getCall = vi . fn (
343+ ( ) : CallRecord => ( {
344+ callId : "call-1" ,
345+ providerCallId : "v3:call-1" ,
346+ provider : "telnyx" ,
347+ direction : "inbound" ,
348+ state : "answered" ,
349+ from : "+15550001234" ,
350+ to : "+15550009999" ,
351+ startedAt : Date . now ( ) ,
352+ transcript : [ ] ,
353+ processedEventIds : [ ] ,
354+ metadata : { } ,
355+ } ) ,
356+ ) ;
357+ const createBridge = vi . fn ( ( ) => makeBridge ( ) ) ;
358+ const handler = makeHandler ( undefined , {
359+ manager : {
360+ processEvent,
361+ getCall,
362+ } ,
363+ provider : {
364+ name : "telnyx" ,
365+ } ,
366+ realtimeProvider : makeRealtimeProvider ( createBridge ) ,
367+ } ) ;
368+ handler . setPublicUrl ( "https://public.example/voice/webhook" ) ;
369+ const session = handler . issueStreamSession ( {
370+ providerName : "telnyx" ,
371+ callId : "call-1" ,
372+ direction : "inbound" ,
373+ } ) ;
374+ const server = await startStreamSessionServer ( handler , session . streamUrl ) ;
375+
376+ try {
377+ const ws = await connectWs ( server . url ) ;
378+ ws . send (
379+ JSON . stringify ( {
380+ event : "start" ,
381+ stream_id : "stream-1" ,
382+ start : { call_control_id : "v3:other" } ,
383+ } ) ,
384+ ) ;
385+ const close = await waitForClose ( ws ) ;
386+
387+ expect ( close . code ) . toBe ( 1008 ) ;
388+ expect ( createBridge ) . not . toHaveBeenCalled ( ) ;
389+ expect ( processEvent ) . not . toHaveBeenCalled ( ) ;
390+ } finally {
391+ await server . close ( ) ;
392+ }
393+ } ) ;
394+
255395 it ( "does not emit an outbound realtime greeting without an initial message" , async ( ) => {
256396 let callbacks :
257397 | {
0 commit comments