






















@@ -87,6 +87,7 @@ function makeHandler(
8787config,
8888{
8989processEvent: vi.fn(),
90+getCall: vi.fn(),
9091getCallByProviderCallId: vi.fn(),
9192 ...deps?.manager,
9293} as unknown as CallManager,
@@ -128,6 +129,21 @@ const startRealtimeServer = async (
128129});
129130};
130131132+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(
132148callback: () => void | Promise<void>,
133149options: { timeout?: number; interval?: number } = {},
@@ -252,6 +268,130 @@ describe("RealtimeCallHandler path routing", () => {
252268}
253269});
254270271+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+255395it("does not emit an outbound realtime greeting without an initial message", async () => {
256396let callbacks:
257397| {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。