
























@@ -315,6 +315,147 @@ describe("TelnyxProvider answer control", () => {
315315});
316316});
317317318+describe("TelnyxProvider Media Streaming (PCMU)", () => {
319+it("embeds streaming fields in the dial payload when streamUrl is provided", async () => {
320+const release = vi.fn(async () => {});
321+apiMocks.fetchWithSsrFGuard.mockResolvedValue({
322+response: new Response(JSON.stringify({ data: { call_control_id: "call-control-1" } }), {
323+status: 200,
324+}),
325+ release,
326+});
327+const provider = new TelnyxProvider({
328+apiKey: "KEY123",
329+connectionId: "CONN456",
330+publicKey: undefined,
331+});
332+333+await provider.initiateCall({
334+callId: "call-1",
335+from: "+15550000001",
336+to: "+15550000002",
337+webhookUrl: "https://example.test/voice/webhook",
338+streamUrl: "wss://example.test/voice/stream/realtime/token-xyz",
339+streamAuthToken: "token-xyz",
340+});
341+342+const request = requireFetchRequest();
343+const body = JSON.parse(request.init?.body as string) as Record<string, unknown>;
344+expect(body.stream_url).toBe("wss://example.test/voice/stream/realtime/token-xyz");
345+expect(body.stream_track).toBe("inbound_track");
346+expect(body.stream_codec).toBe("PCMU");
347+expect(body.stream_bidirectional_mode).toBe("rtp");
348+expect(body.stream_bidirectional_codec).toBe("PCMU");
349+expect(body.stream_bidirectional_sampling_rate).toBe(8000);
350+expect(body.stream_bidirectional_target_legs).toBe("self");
351+expect(body.stream_auth_token).toBe("token-xyz");
352+});
353+354+it("omits streaming fields from the dial payload when streamUrl is absent", async () => {
355+apiMocks.fetchWithSsrFGuard.mockResolvedValue({
356+response: new Response(JSON.stringify({ data: { call_control_id: "call-control-1" } }), {
357+status: 200,
358+}),
359+release: vi.fn(async () => {}),
360+});
361+const provider = new TelnyxProvider({
362+apiKey: "KEY123",
363+connectionId: "CONN456",
364+publicKey: undefined,
365+});
366+367+await provider.initiateCall({
368+callId: "call-1",
369+from: "+15550000001",
370+to: "+15550000002",
371+webhookUrl: "https://example.test/voice/webhook",
372+});
373+374+const body = JSON.parse(requireFetchRequest().init?.body as string) as Record<string, unknown>;
375+expect(body.stream_url).toBeUndefined();
376+expect(body.stream_codec).toBeUndefined();
377+expect(body.stream_bidirectional_codec).toBeUndefined();
378+expect(body.stream_auth_token).toBeUndefined();
379+});
380+381+it("embeds streaming fields in the answer action when streamUrl is provided", async () => {
382+apiMocks.fetchWithSsrFGuard.mockResolvedValue({
383+response: new Response(JSON.stringify({ data: {} }), { status: 200 }),
384+release: vi.fn(async () => {}),
385+});
386+const provider = new TelnyxProvider({
387+apiKey: "KEY123",
388+connectionId: "CONN456",
389+publicKey: undefined,
390+});
391+392+await provider.answerCall({
393+callId: "call-1",
394+providerCallId: "call-control-1",
395+streamUrl: "wss://example.test/voice/stream/realtime/token-xyz",
396+streamAuthToken: "token-xyz",
397+});
398+399+const body = JSON.parse(requireFetchRequest().init?.body as string) as Record<string, unknown>;
400+expect(body.command_id).toBe("openclaw-answer-call-1");
401+expect(body.stream_url).toBe("wss://example.test/voice/stream/realtime/token-xyz");
402+expect(body.stream_codec).toBe("PCMU");
403+expect(body.stream_bidirectional_target_legs).toBe("self");
404+expect(body.stream_auth_token).toBe("token-xyz");
405+});
406+407+it("normalizes call.streaming.failed into a non-retryable call.error event", () => {
408+const provider = new TelnyxProvider(
409+{ apiKey: "KEY123", connectionId: "CONN456", publicKey: undefined },
410+{ skipVerification: true },
411+);
412+const rawBody = JSON.stringify({
413+data: {
414+event_type: "call.streaming.failed",
415+id: "evt-1",
416+payload: {
417+call_control_id: "call-control-1",
418+client_state: Buffer.from("call-1").toString("base64"),
419+reason: "destination_unreachable",
420+},
421+},
422+});
423+const result = provider.parseWebhookEvent(createCtx({ rawBody }), {
424+verifiedRequestKey: "key-1",
425+});
426+expect(result.events).toHaveLength(1);
427+const event = result.events[0];
428+if (event?.type !== "call.error") {
429+throw new Error(`expected call.error, got ${event?.type}`);
430+}
431+expect(event.error).toBe("Telnyx streaming failed: destination_unreachable");
432+expect(event.retryable).toBe(false);
433+expect(event.callId).toBe("call-1");
434+expect(event.providerCallId).toBe("call-control-1");
435+});
436+437+it("silently acknowledges call.streaming.started and call.streaming.stopped", () => {
438+const provider = new TelnyxProvider(
439+{ apiKey: "KEY123", connectionId: "CONN456", publicKey: undefined },
440+{ skipVerification: true },
441+);
442+for (const eventType of ["call.streaming.started", "call.streaming.stopped"]) {
443+const rawBody = JSON.stringify({
444+data: {
445+event_type: eventType,
446+id: `evt-${eventType}`,
447+payload: { call_control_id: "call-control-1" },
448+},
449+});
450+const result = provider.parseWebhookEvent(createCtx({ rawBody }), {
451+verifiedRequestKey: "key-1",
452+});
453+expect(result.events).toHaveLength(0);
454+expect(result.statusCode).toBe(200);
455+}
456+});
457+});
458+318459describe("TelnyxProvider speak control", () => {
319460it("passes custom Telnyx voice ids to the speak action", async () => {
320461const release = vi.fn(async () => {});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。