@kuralle-syrinx/server-websocket 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/backchannel-cue-fixtures.d.ts +4 -0
  2. package/dist/backchannel-cue-fixtures.d.ts.map +1 -0
  3. package/dist/backchannel-cue-fixtures.js +25 -0
  4. package/dist/backchannel-cue-fixtures.js.map +1 -0
  5. package/dist/background-audio.d.ts +59 -0
  6. package/dist/background-audio.d.ts.map +1 -0
  7. package/dist/background-audio.js +248 -0
  8. package/dist/background-audio.js.map +1 -0
  9. package/dist/browser-opus.d.ts +12 -0
  10. package/dist/browser-opus.d.ts.map +1 -0
  11. package/dist/browser-opus.js +48 -0
  12. package/dist/browser-opus.js.map +1 -0
  13. package/dist/carrier-commands.d.ts +124 -0
  14. package/dist/carrier-commands.d.ts.map +1 -0
  15. package/dist/carrier-commands.js +193 -0
  16. package/dist/carrier-commands.js.map +1 -0
  17. package/dist/edge-telnyx.d.ts +37 -0
  18. package/dist/edge-telnyx.d.ts.map +1 -0
  19. package/dist/edge-telnyx.js +320 -0
  20. package/dist/edge-telnyx.js.map +1 -0
  21. package/dist/edge-twilio.d.ts +31 -0
  22. package/dist/edge-twilio.d.ts.map +1 -0
  23. package/dist/edge-twilio.js +286 -0
  24. package/dist/edge-twilio.js.map +1 -0
  25. package/dist/edge.d.ts +71 -0
  26. package/dist/edge.d.ts.map +1 -0
  27. package/dist/edge.js +560 -0
  28. package/dist/edge.js.map +1 -0
  29. package/dist/inbound-audio.d.ts +13 -0
  30. package/dist/inbound-audio.d.ts.map +1 -0
  31. package/dist/inbound-audio.js +66 -0
  32. package/dist/inbound-audio.js.map +1 -0
  33. package/dist/index.d.ts +81 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +718 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/json-message.d.ts +7 -0
  38. package/dist/json-message.d.ts.map +1 -0
  39. package/dist/json-message.js +41 -0
  40. package/dist/json-message.js.map +1 -0
  41. package/dist/outbound-playout-pipeline.d.ts +53 -0
  42. package/dist/outbound-playout-pipeline.d.ts.map +1 -0
  43. package/dist/outbound-playout-pipeline.js +148 -0
  44. package/dist/outbound-playout-pipeline.js.map +1 -0
  45. package/dist/paced-playout.d.ts +42 -0
  46. package/dist/paced-playout.d.ts.map +1 -0
  47. package/dist/paced-playout.js +151 -0
  48. package/dist/paced-playout.js.map +1 -0
  49. package/dist/playout-progress.d.ts +26 -0
  50. package/dist/playout-progress.d.ts.map +1 -0
  51. package/dist/playout-progress.js +64 -0
  52. package/dist/playout-progress.js.map +1 -0
  53. package/dist/session-store.d.ts +39 -0
  54. package/dist/session-store.d.ts.map +1 -0
  55. package/dist/session-store.js +80 -0
  56. package/dist/session-store.js.map +1 -0
  57. package/dist/smartpbx.d.ts +47 -0
  58. package/dist/smartpbx.d.ts.map +1 -0
  59. package/dist/smartpbx.js +437 -0
  60. package/dist/smartpbx.js.map +1 -0
  61. package/dist/telnyx-codec.d.ts +41 -0
  62. package/dist/telnyx-codec.d.ts.map +1 -0
  63. package/dist/telnyx-codec.js +114 -0
  64. package/dist/telnyx-codec.js.map +1 -0
  65. package/dist/telnyx.d.ts +52 -0
  66. package/dist/telnyx.d.ts.map +1 -0
  67. package/dist/telnyx.js +550 -0
  68. package/dist/telnyx.js.map +1 -0
  69. package/dist/test-helpers.d.ts +20 -0
  70. package/dist/test-helpers.d.ts.map +1 -0
  71. package/dist/test-helpers.js +242 -0
  72. package/dist/test-helpers.js.map +1 -0
  73. package/dist/transport-helpers.d.ts +12 -0
  74. package/dist/transport-helpers.d.ts.map +1 -0
  75. package/dist/transport-helpers.js +90 -0
  76. package/dist/transport-helpers.js.map +1 -0
  77. package/dist/transport-host.d.ts +56 -0
  78. package/dist/transport-host.d.ts.map +1 -0
  79. package/dist/transport-host.js +110 -0
  80. package/dist/transport-host.js.map +1 -0
  81. package/dist/turn-metrics.d.ts +79 -0
  82. package/dist/turn-metrics.d.ts.map +1 -0
  83. package/dist/turn-metrics.js +208 -0
  84. package/dist/turn-metrics.js.map +1 -0
  85. package/dist/twilio-auth.d.ts +12 -0
  86. package/dist/twilio-auth.d.ts.map +1 -0
  87. package/dist/twilio-auth.js +44 -0
  88. package/dist/twilio-auth.js.map +1 -0
  89. package/dist/twilio.d.ts +56 -0
  90. package/dist/twilio.d.ts.map +1 -0
  91. package/dist/twilio.js +511 -0
  92. package/dist/twilio.js.map +1 -0
  93. package/dist/websocket-close.d.ts +12 -0
  94. package/dist/websocket-close.d.ts.map +1 -0
  95. package/dist/websocket-close.js +58 -0
  96. package/dist/websocket-close.js.map +1 -0
  97. package/dist/websocket-lifecycle.d.ts +29 -0
  98. package/dist/websocket-lifecycle.d.ts.map +1 -0
  99. package/dist/websocket-lifecycle.js +87 -0
  100. package/dist/websocket-lifecycle.js.map +1 -0
  101. package/dist/websocket-upgrade.d.ts +9 -0
  102. package/dist/websocket-upgrade.d.ts.map +1 -0
  103. package/dist/websocket-upgrade.js +91 -0
  104. package/dist/websocket-upgrade.js.map +1 -0
  105. package/dist/wire-carrier-control.d.ts +27 -0
  106. package/dist/wire-carrier-control.d.ts.map +1 -0
  107. package/dist/wire-carrier-control.js +116 -0
  108. package/dist/wire-carrier-control.js.map +1 -0
  109. package/package.json +32 -11
  110. package/src/admission-control.test.ts +270 -0
  111. package/src/background-audio.test.ts +224 -0
  112. package/src/browser-opus.test.ts +41 -0
  113. package/src/browser-pacing.test.ts +440 -0
  114. package/src/carrier-commands.test.ts +198 -0
  115. package/src/edge-telnyx.test.ts +333 -0
  116. package/src/edge-twilio.test.ts +293 -0
  117. package/src/edge.test.ts +727 -0
  118. package/src/graceful-drain.test.ts +306 -0
  119. package/src/inbound-audio.test.ts +58 -0
  120. package/src/index.test.ts +2078 -0
  121. package/src/outbound-playout-pipeline.test.ts +314 -0
  122. package/src/paced-playout.test.ts +247 -0
  123. package/src/playout-progress.test.ts +56 -0
  124. package/src/session-store.test.ts +274 -0
  125. package/src/smartpbx.test.ts +986 -0
  126. package/src/telnyx.test.ts +1633 -0
  127. package/src/transport-host.test.ts +51 -0
  128. package/src/turn-metrics.test.ts +503 -0
  129. package/src/twilio-auth.test.ts +36 -0
  130. package/src/twilio.test.ts +1274 -0
  131. package/src/websocket-close.test.ts +63 -0
  132. package/src/websocket-lifecycle.test.ts +49 -0
@@ -0,0 +1,1633 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import WebSocket from "ws";
5
+ import { Route, VoiceAgentSession, dtmfSend, callTransfer, type ConversationMetricPacket, type RecordAssistantAudioPacket, type TextToSpeechPlayoutProgressPacket, type UserAudioReceivedPacket } from "@kuralle-syrinx/core";
6
+ import { createTelnyxMediaStreamServer, validateTelnyxStart } from "./telnyx.js";
7
+ import {
8
+ createG722EncoderState,
9
+ decodeMuLawToPcm16,
10
+ encodeG722,
11
+ encodePcm16ToALaw,
12
+ encodePcm16ToMuLaw,
13
+ pcm16SamplesToBytes,
14
+ resamplePcm16,
15
+ } from "@kuralle-syrinx/core/audio";
16
+ import {
17
+ openSocket,
18
+ readJsonMatching,
19
+ registerServer,
20
+ setupTransportTestCleanup,
21
+ waitForCondition,
22
+ } from "./test-helpers.js";
23
+
24
+ setupTransportTestCleanup();
25
+
26
+ function telnyxUrl(port: number): string {
27
+ return `ws://127.0.0.1:${port}/telnyx`;
28
+ }
29
+
30
+ function telnyxStart(encoding = "PCMU", sampleRate = 8000): Record<string, unknown> {
31
+ return {
32
+ event: "start",
33
+ stream_id: "telnyx-stream",
34
+ start: {
35
+ stream_id: "telnyx-stream",
36
+ call_control_id: "call-control-test",
37
+ media_format: {
38
+ encoding,
39
+ sample_rate: sampleRate,
40
+ channels: 1,
41
+ },
42
+ },
43
+ };
44
+ }
45
+
46
+ function bigEndianPcm16(samples: Int16Array): Uint8Array {
47
+ const output = new Uint8Array(samples.byteLength);
48
+ const view = new DataView(output.buffer);
49
+ for (let i = 0; i < samples.length; i += 1) {
50
+ view.setInt16(i * 2, samples[i]!, false);
51
+ }
52
+ return output;
53
+ }
54
+
55
+ describe("createTelnyxMediaStreamServer", () => {
56
+ it("decodes Telnyx PCMU media frames into engine PCM16 audio packets", async () => {
57
+ const session = new VoiceAgentSession({ plugins: {} });
58
+ const received: UserAudioReceivedPacket[] = [];
59
+ session.bus.on("user.audio_received", (pkt) => {
60
+ received.push(pkt as UserAudioReceivedPacket);
61
+ });
62
+
63
+ const server = registerServer(await createTelnyxMediaStreamServer({
64
+ port: 0,
65
+ inputSampleRateHz: 16000,
66
+ createSession: () => session,
67
+ }));
68
+ const address = server.address();
69
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
70
+
71
+ const client = await openSocket(telnyxUrl(address.port));
72
+ const samples8k = new Int16Array([0, 1000, -1000, 3000]);
73
+ const ulaw = encodePcm16ToMuLaw(samples8k);
74
+
75
+ client.send(JSON.stringify(telnyxStart()));
76
+ client.send(JSON.stringify({
77
+ event: "media",
78
+ stream_id: "telnyx-stream",
79
+ media: {
80
+ payload: Buffer.from(ulaw).toString("base64"),
81
+ },
82
+ }));
83
+ await waitForCondition(() => received.length === 1);
84
+
85
+ const decoded = decodeMuLawToPcm16(ulaw);
86
+ const expected = pcm16SamplesToBytes(resamplePcm16(decoded, 8000, 16000));
87
+ expect(received).toEqual([
88
+ expect.objectContaining({
89
+ kind: "user.audio_received",
90
+ contextId: "telnyx-call-control-test",
91
+ }),
92
+ ]);
93
+ expect(Buffer.from(received[0]!.audio)).toEqual(Buffer.from(expected));
94
+
95
+ client.close();
96
+ await server.close();
97
+ });
98
+
99
+ it("records Telnyx media chunk gaps after the bounded reorder window is exceeded", async () => {
100
+ const session = new VoiceAgentSession({ plugins: {} });
101
+ const received: UserAudioReceivedPacket[] = [];
102
+ const metrics: ConversationMetricPacket[] = [];
103
+ session.bus.on("user.audio_received", (pkt) => {
104
+ received.push(pkt as UserAudioReceivedPacket);
105
+ });
106
+ session.bus.on("metric.conversation", (pkt) => {
107
+ metrics.push(pkt as ConversationMetricPacket);
108
+ });
109
+
110
+ const server = registerServer(await createTelnyxMediaStreamServer({
111
+ port: 0,
112
+ inputSampleRateHz: 16000,
113
+ maxInboundReorderFrames: 1,
114
+ createSession: () => session,
115
+ }));
116
+ const address = server.address();
117
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
118
+
119
+ const client = await openSocket(telnyxUrl(address.port));
120
+ const payload = Buffer.from(encodePcm16ToMuLaw(new Int16Array([0, 1000, -1000, 3000]))).toString("base64");
121
+
122
+ client.send(JSON.stringify(telnyxStart()));
123
+ client.send(JSON.stringify({
124
+ event: "media",
125
+ stream_id: "telnyx-stream",
126
+ media: { chunk: "1", timestamp: "20", payload },
127
+ }));
128
+ client.send(JSON.stringify({
129
+ event: "media",
130
+ stream_id: "telnyx-stream",
131
+ media: { chunk: "4", timestamp: "80", payload },
132
+ }));
133
+ client.send(JSON.stringify({
134
+ event: "media",
135
+ stream_id: "telnyx-stream",
136
+ media: { chunk: "5", timestamp: "100", payload },
137
+ }));
138
+ await waitForCondition(() => received.length === 3);
139
+
140
+ expect(received).toHaveLength(3);
141
+ expect(metrics).toContainEqual(expect.objectContaining({
142
+ kind: "metric.conversation",
143
+ contextId: "telnyx-call-control-test",
144
+ name: "telnyx.media_chunk_gap",
145
+ value: JSON.stringify({ expected: 2, actual: 4, missed: 2 }),
146
+ }));
147
+
148
+ client.close();
149
+ await server.close();
150
+ });
151
+
152
+ it("reorders out-of-order Telnyx media chunks before forwarding audio", async () => {
153
+ const session = new VoiceAgentSession({ plugins: {} });
154
+ const received: UserAudioReceivedPacket[] = [];
155
+ session.bus.on("user.audio_received", (pkt) => {
156
+ received.push(pkt as UserAudioReceivedPacket);
157
+ });
158
+
159
+ const server = registerServer(await createTelnyxMediaStreamServer({
160
+ port: 0,
161
+ inputSampleRateHz: 16000,
162
+ createSession: () => session,
163
+ }));
164
+ const address = server.address();
165
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
166
+
167
+ const client = await openSocket(telnyxUrl(address.port));
168
+ const chunk1 = bigEndianPcm16(new Int16Array([1, 2, 3, 4]));
169
+ const chunk2 = bigEndianPcm16(new Int16Array([101, 102, 103, 104]));
170
+ const chunk3 = bigEndianPcm16(new Int16Array([201, 202, 203, 204]));
171
+
172
+ client.send(JSON.stringify(telnyxStart("L16", 16000)));
173
+ client.send(JSON.stringify({
174
+ event: "media",
175
+ stream_id: "telnyx-stream",
176
+ media: { chunk: "1", timestamp: "0", payload: Buffer.from(chunk1).toString("base64") },
177
+ }));
178
+ client.send(JSON.stringify({
179
+ event: "media",
180
+ stream_id: "telnyx-stream",
181
+ media: { chunk: "3", timestamp: "40", payload: Buffer.from(chunk3).toString("base64") },
182
+ }));
183
+ client.send(JSON.stringify({
184
+ event: "media",
185
+ stream_id: "telnyx-stream",
186
+ media: { chunk: "2", timestamp: "20", payload: Buffer.from(chunk2).toString("base64") },
187
+ }));
188
+ await waitForCondition(() => received.length === 3);
189
+
190
+ expect(received).toHaveLength(3);
191
+ expect(Buffer.from(received[0]!.audio)).toEqual(Buffer.from(pcm16SamplesToBytes(new Int16Array([1, 2, 3, 4]))));
192
+ expect(Buffer.from(received[1]!.audio)).toEqual(Buffer.from(pcm16SamplesToBytes(new Int16Array([101, 102, 103, 104]))));
193
+ expect(Buffer.from(received[2]!.audio)).toEqual(Buffer.from(pcm16SamplesToBytes(new Int16Array([201, 202, 203, 204]))));
194
+
195
+ client.close();
196
+ await server.close();
197
+ });
198
+
199
+ it("flushes buffered Telnyx media chunks when the websocket disconnects before stop", async () => {
200
+ const session = new VoiceAgentSession({ plugins: {} });
201
+ const received: UserAudioReceivedPacket[] = [];
202
+ const metrics: ConversationMetricPacket[] = [];
203
+ let closeCalls = 0;
204
+ session.bus.on("user.audio_received", (pkt) => {
205
+ received.push(pkt as UserAudioReceivedPacket);
206
+ });
207
+ session.bus.on("metric.conversation", (pkt) => {
208
+ metrics.push(pkt as ConversationMetricPacket);
209
+ });
210
+ const originalClose = session.close.bind(session);
211
+ session.close = async () => {
212
+ closeCalls += 1;
213
+ await originalClose();
214
+ };
215
+
216
+ const server = registerServer(await createTelnyxMediaStreamServer({
217
+ port: 0,
218
+ inputSampleRateHz: 16000,
219
+ createSession: () => session,
220
+ }));
221
+ const address = server.address();
222
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
223
+
224
+ const client = await openSocket(telnyxUrl(address.port));
225
+ const chunk1 = bigEndianPcm16(new Int16Array([1, 2, 3, 4]));
226
+ const chunk3 = bigEndianPcm16(new Int16Array([201, 202, 203, 204]));
227
+
228
+ client.send(JSON.stringify(telnyxStart("L16", 16000)));
229
+ client.send(JSON.stringify({
230
+ event: "media",
231
+ stream_id: "telnyx-stream",
232
+ media: { chunk: "1", timestamp: "0", payload: Buffer.from(chunk1).toString("base64") },
233
+ }));
234
+ client.send(JSON.stringify({
235
+ event: "media",
236
+ stream_id: "telnyx-stream",
237
+ media: { chunk: "3", timestamp: "40", payload: Buffer.from(chunk3).toString("base64") },
238
+ }));
239
+ await waitForCondition(() => received.length === 1);
240
+ expect(received).toHaveLength(1);
241
+
242
+ client.close();
243
+ await waitForCondition(() => received.length === 2 && closeCalls > 0);
244
+
245
+ expect(closeCalls).toBeGreaterThan(0);
246
+ expect(received).toHaveLength(2);
247
+ expect(Buffer.from(received[1]!.audio)).toEqual(Buffer.from(pcm16SamplesToBytes(new Int16Array([201, 202, 203, 204]))));
248
+ expect(metrics).toContainEqual(expect.objectContaining({
249
+ kind: "metric.conversation",
250
+ contextId: "telnyx-call-control-test",
251
+ name: "telnyx.media_chunk_gap",
252
+ value: JSON.stringify({ expected: 2, actual: 3, missed: 1 }),
253
+ }));
254
+
255
+ await server.close();
256
+ });
257
+
258
+ it("records Telnyx media timestamp gaps and regressions without dropping media", async () => {
259
+ const session = new VoiceAgentSession({ plugins: {} });
260
+ const received: UserAudioReceivedPacket[] = [];
261
+ const metrics: ConversationMetricPacket[] = [];
262
+ session.bus.on("user.audio_received", (pkt) => {
263
+ received.push(pkt as UserAudioReceivedPacket);
264
+ });
265
+ session.bus.on("metric.conversation", (pkt) => {
266
+ metrics.push(pkt as ConversationMetricPacket);
267
+ });
268
+
269
+ const server = registerServer(await createTelnyxMediaStreamServer({
270
+ port: 0,
271
+ inputSampleRateHz: 16000,
272
+ createSession: () => session,
273
+ }));
274
+ const address = server.address();
275
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
276
+
277
+ const client = await openSocket(telnyxUrl(address.port));
278
+ const payload = Buffer.from(encodePcm16ToMuLaw(new Int16Array(160))).toString("base64");
279
+
280
+ client.send(JSON.stringify(telnyxStart()));
281
+ client.send(JSON.stringify({
282
+ event: "media",
283
+ stream_id: "telnyx-stream",
284
+ media: { chunk: "1", timestamp: "0", payload },
285
+ }));
286
+ client.send(JSON.stringify({
287
+ event: "media",
288
+ stream_id: "telnyx-stream",
289
+ media: { chunk: "2", timestamp: "60", payload },
290
+ }));
291
+ client.send(JSON.stringify({
292
+ event: "media",
293
+ stream_id: "telnyx-stream",
294
+ media: { chunk: "3", timestamp: "40", payload },
295
+ }));
296
+ await new Promise((resolve) => setTimeout(resolve, 20));
297
+
298
+ expect(received).toHaveLength(3);
299
+ expect(metrics).toContainEqual(expect.objectContaining({
300
+ kind: "metric.conversation",
301
+ contextId: "telnyx-call-control-test",
302
+ name: "telnyx.media_timestamp_gap",
303
+ value: JSON.stringify({ expected: 20, actual: 60, missedMs: 40 }),
304
+ }));
305
+ expect(metrics).toContainEqual(expect.objectContaining({
306
+ kind: "metric.conversation",
307
+ contextId: "telnyx-call-control-test",
308
+ name: "telnyx.media_timestamp_regression",
309
+ value: JSON.stringify({ previous: 60, actual: 40 }),
310
+ }));
311
+
312
+ client.close();
313
+ await server.close();
314
+ });
315
+
316
+ it("drops duplicate Telnyx media chunks with a metric instead of erroring", async () => {
317
+ const session = new VoiceAgentSession({ plugins: {} });
318
+ const received: UserAudioReceivedPacket[] = [];
319
+ const metrics: ConversationMetricPacket[] = [];
320
+ session.bus.on("user.audio_received", (pkt) => {
321
+ received.push(pkt as UserAudioReceivedPacket);
322
+ });
323
+ session.bus.on("metric.conversation", (pkt) => {
324
+ metrics.push(pkt as ConversationMetricPacket);
325
+ });
326
+
327
+ const server = registerServer(await createTelnyxMediaStreamServer({
328
+ port: 0,
329
+ inputSampleRateHz: 16000,
330
+ createSession: () => session,
331
+ }));
332
+ const address = server.address();
333
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
334
+
335
+ const client = await openSocket(telnyxUrl(address.port));
336
+ const payload = Buffer.from(encodePcm16ToMuLaw(new Int16Array([0, 1000, -1000, 3000]))).toString("base64");
337
+
338
+ client.send(JSON.stringify(telnyxStart()));
339
+ client.send(JSON.stringify({
340
+ event: "media",
341
+ stream_id: "telnyx-stream",
342
+ media: { chunk: "1", timestamp: "20", payload },
343
+ }));
344
+ await new Promise((resolve) => setTimeout(resolve, 20));
345
+ client.send(JSON.stringify({
346
+ event: "media",
347
+ stream_id: "telnyx-stream",
348
+ media: { chunk: "1", timestamp: "20", payload },
349
+ }));
350
+ await new Promise((resolve) => setTimeout(resolve, 20));
351
+
352
+ expect(received).toHaveLength(1);
353
+ expect(metrics).toContainEqual(expect.objectContaining({
354
+ name: "telnyx.media_chunk_duplicate",
355
+ value: JSON.stringify({ expected: 2, received: 1 }),
356
+ }));
357
+
358
+ client.close();
359
+ await server.close();
360
+ });
361
+
362
+ it("records Telnyx top-level sequence_number gaps", async () => {
363
+ const session = new VoiceAgentSession({ plugins: {} });
364
+ const metrics: ConversationMetricPacket[] = [];
365
+ session.bus.on("metric.conversation", (pkt) => {
366
+ metrics.push(pkt as ConversationMetricPacket);
367
+ });
368
+
369
+ const server = registerServer(await createTelnyxMediaStreamServer({
370
+ port: 0,
371
+ inputSampleRateHz: 16000,
372
+ createSession: () => session,
373
+ }));
374
+ const address = server.address();
375
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
376
+
377
+ const client = await openSocket(telnyxUrl(address.port));
378
+ const payload = Buffer.from(encodePcm16ToMuLaw(new Int16Array([0, 1000, -1000, 3000]))).toString("base64");
379
+
380
+ client.send(JSON.stringify({ ...telnyxStart(), sequence_number: "1" }));
381
+ client.send(JSON.stringify({
382
+ event: "media",
383
+ sequence_number: "4",
384
+ stream_id: "telnyx-stream",
385
+ media: { chunk: "1", timestamp: "20", payload },
386
+ }));
387
+ await new Promise((resolve) => setTimeout(resolve, 20));
388
+
389
+ expect(metrics).toContainEqual(expect.objectContaining({
390
+ kind: "metric.conversation",
391
+ contextId: "telnyx-call-control-test",
392
+ name: "telnyx.sequence_gap",
393
+ value: JSON.stringify({ expected: 2, actual: 4, missed: 2 }),
394
+ }));
395
+
396
+ client.close();
397
+ await server.close();
398
+ });
399
+
400
+ it("records regressing Telnyx top-level sequence_number without dropping media", async () => {
401
+ const session = new VoiceAgentSession({ plugins: {} });
402
+ const received: UserAudioReceivedPacket[] = [];
403
+ const metrics: ConversationMetricPacket[] = [];
404
+ session.bus.on("user.audio_received", (pkt) => {
405
+ received.push(pkt as UserAudioReceivedPacket);
406
+ });
407
+ session.bus.on("metric.conversation", (pkt) => {
408
+ metrics.push(pkt as ConversationMetricPacket);
409
+ });
410
+
411
+ const server = registerServer(await createTelnyxMediaStreamServer({
412
+ port: 0,
413
+ inputSampleRateHz: 16000,
414
+ createSession: () => session,
415
+ }));
416
+ const address = server.address();
417
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
418
+
419
+ const client = await openSocket(telnyxUrl(address.port));
420
+ const payload = Buffer.from(encodePcm16ToMuLaw(new Int16Array([0, 1000, -1000, 3000]))).toString("base64");
421
+
422
+ client.send(JSON.stringify({ ...telnyxStart(), sequence_number: "1" }));
423
+ client.send(JSON.stringify({
424
+ event: "media",
425
+ sequence_number: "3",
426
+ stream_id: "telnyx-stream",
427
+ media: { chunk: "1", timestamp: "20", payload },
428
+ }));
429
+ client.send(JSON.stringify({
430
+ event: "media",
431
+ sequence_number: "2",
432
+ stream_id: "telnyx-stream",
433
+ media: { chunk: "2", timestamp: "40", payload },
434
+ }));
435
+ await new Promise((resolve) => setTimeout(resolve, 20));
436
+
437
+ expect(received).toHaveLength(2);
438
+ expect(metrics).toContainEqual(expect.objectContaining({
439
+ kind: "metric.conversation",
440
+ contextId: "telnyx-call-control-test",
441
+ name: "telnyx.sequence_gap",
442
+ value: JSON.stringify({ expected: 2, actual: 3, missed: 1 }),
443
+ }));
444
+ expect(metrics).toContainEqual(expect.objectContaining({
445
+ kind: "metric.conversation",
446
+ contextId: "telnyx-call-control-test",
447
+ name: "telnyx.sequence_regression",
448
+ value: JSON.stringify({ previous: 3, actual: 2 }),
449
+ }));
450
+
451
+ client.close();
452
+ await server.close();
453
+ });
454
+
455
+ it("buffers Telnyx start and media sent before session startup completes", async () => {
456
+ const session = new VoiceAgentSession({ plugins: {} });
457
+ const received: UserAudioReceivedPacket[] = [];
458
+ session.bus.on("user.audio_received", (pkt) => {
459
+ received.push(pkt as UserAudioReceivedPacket);
460
+ });
461
+
462
+ const server = registerServer(await createTelnyxMediaStreamServer({
463
+ port: 0,
464
+ inputSampleRateHz: 16000,
465
+ createSession: async () => {
466
+ await new Promise((resolve) => setTimeout(resolve, 30));
467
+ return session;
468
+ },
469
+ }));
470
+ const address = server.address();
471
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
472
+
473
+ const client = await openSocket(telnyxUrl(address.port));
474
+ const ulaw = encodePcm16ToMuLaw(new Int16Array([0, 1000, -1000, 3000]));
475
+ client.send(JSON.stringify(telnyxStart()));
476
+ client.send(JSON.stringify({
477
+ event: "media",
478
+ stream_id: "telnyx-stream",
479
+ media: {
480
+ payload: Buffer.from(ulaw).toString("base64"),
481
+ },
482
+ }));
483
+ await new Promise((resolve) => setTimeout(resolve, 80));
484
+
485
+ expect(received).toEqual([
486
+ expect.objectContaining({
487
+ kind: "user.audio_received",
488
+ contextId: "telnyx-call-control-test",
489
+ }),
490
+ ]);
491
+
492
+ client.close();
493
+ await server.close();
494
+ });
495
+
496
+ it("closes Telnyx websocket connections when session startup exceeds startupTimeoutMs", async () => {
497
+ const server = registerServer(await createTelnyxMediaStreamServer({
498
+ port: 0,
499
+ startupTimeoutMs: 10,
500
+ createSession: () => new Promise<VoiceAgentSession>(() => undefined),
501
+ }));
502
+ const address = server.address();
503
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
504
+
505
+ const client = await openSocket(telnyxUrl(address.port));
506
+ const errorMessage = readJsonMatching(client, (message) => message.event === "error");
507
+ const closed = new Promise<{ code: number; reason: string }>((resolve) => {
508
+ client.once("close", (code, reason) => {
509
+ resolve({ code, reason: reason.toString() });
510
+ });
511
+ });
512
+
513
+ await expect(errorMessage).resolves.toEqual(expect.objectContaining({ event: expect.any(String) }));
514
+ await expect(closed).resolves.toEqual({
515
+ code: 1011,
516
+ reason: "session initialization failed",
517
+ });
518
+
519
+ await server.close();
520
+ });
521
+
522
+ it("rejects unsupported Telnyx start media formats before forwarding audio", async () => {
523
+ const session = new VoiceAgentSession({ plugins: {} });
524
+ const received: UserAudioReceivedPacket[] = [];
525
+ session.bus.on("user.audio_received", (pkt) => {
526
+ received.push(pkt as UserAudioReceivedPacket);
527
+ });
528
+
529
+ const server = registerServer(await createTelnyxMediaStreamServer({
530
+ port: 0,
531
+ createSession: () => session,
532
+ }));
533
+ const address = server.address();
534
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
535
+
536
+ const client = await openSocket(telnyxUrl(address.port));
537
+ const errorMessage = readJsonMatching(client, (message) => message.event === "error");
538
+ client.send(JSON.stringify(telnyxStart("PCMU", 16000)));
539
+
540
+ await expect(errorMessage).resolves.toMatchObject({
541
+ event: "error",
542
+ payload: {
543
+ title: "syrinx_transport_error",
544
+ detail: "Unsupported Telnyx PCMU sample rate: 16000",
545
+ },
546
+ });
547
+ expect(received).toEqual([]);
548
+
549
+ client.close();
550
+ await server.close();
551
+ });
552
+
553
+ it("decodes Telnyx L16 media into native engine PCM16", async () => {
554
+ const session = new VoiceAgentSession({ plugins: {} });
555
+ const received: UserAudioReceivedPacket[] = [];
556
+ session.bus.on("user.audio_received", (pkt) => {
557
+ received.push(pkt as UserAudioReceivedPacket);
558
+ });
559
+
560
+ const server = registerServer(await createTelnyxMediaStreamServer({
561
+ port: 0,
562
+ inputSampleRateHz: 16000,
563
+ createSession: () => session,
564
+ }));
565
+ const address = server.address();
566
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
567
+
568
+ const client = await openSocket(telnyxUrl(address.port));
569
+ const samples = new Int16Array([0, 1000, -1000, 3000]);
570
+ client.send(JSON.stringify(telnyxStart("L16", 16000)));
571
+ client.send(JSON.stringify({
572
+ event: "media",
573
+ stream_id: "telnyx-stream",
574
+ media: {
575
+ payload: Buffer.from(bigEndianPcm16(samples)).toString("base64"),
576
+ },
577
+ }));
578
+ await new Promise((resolve) => setTimeout(resolve, 20));
579
+
580
+ expect(Buffer.from(received[0]!.audio)).toEqual(Buffer.from(pcm16SamplesToBytes(samples)));
581
+
582
+ client.close();
583
+ await server.close();
584
+ });
585
+
586
+ it("treats Telnyx stop as a terminal stream boundary", async () => {
587
+ const session = new VoiceAgentSession({ plugins: {} });
588
+ const received: UserAudioReceivedPacket[] = [];
589
+ session.bus.on("user.audio_received", (pkt) => {
590
+ received.push(pkt as UserAudioReceivedPacket);
591
+ });
592
+
593
+ const server = registerServer(await createTelnyxMediaStreamServer({
594
+ port: 0,
595
+ createSession: () => session,
596
+ }));
597
+ const address = server.address();
598
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
599
+
600
+ const client = await openSocket(telnyxUrl(address.port));
601
+ const messages: any[] = [];
602
+ client.on("message", (data, isBinary) => {
603
+ if (!isBinary) messages.push(JSON.parse(data.toString()));
604
+ });
605
+
606
+ client.send(JSON.stringify(telnyxStart()));
607
+ client.send(JSON.stringify({ event: "stop", stream_id: "telnyx-stream" }));
608
+ client.send(JSON.stringify({
609
+ event: "media",
610
+ stream_id: "telnyx-stream",
611
+ media: {
612
+ payload: Buffer.from(encodePcm16ToMuLaw(new Int16Array([0, 1000, -1000, 3000]))).toString("base64"),
613
+ },
614
+ }));
615
+ session.bus.push(Route.Main, {
616
+ kind: "tts.audio",
617
+ contextId: "telnyx-call-control-test",
618
+ timestampMs: Date.now(),
619
+ audio: pcm16SamplesToBytes(new Int16Array(320)),
620
+ sampleRateHz: 16000,
621
+ });
622
+ await new Promise((resolve) => setTimeout(resolve, 40));
623
+
624
+ expect(received).toHaveLength(0);
625
+ expect(messages.filter((message) => message.event === "media")).toHaveLength(0);
626
+ expect(messages.filter((message) => message.event === "mark")).toHaveLength(0);
627
+ expect(messages.filter((message) => message.event === "clear")).toHaveLength(0);
628
+
629
+ client.close();
630
+ await server.close();
631
+ });
632
+
633
+ it("cancels queued Telnyx playout when the provider sends stop", async () => {
634
+ const session = new VoiceAgentSession({ plugins: {} });
635
+ const metrics: ConversationMetricPacket[] = [];
636
+ const recording: RecordAssistantAudioPacket[] = [];
637
+ let releaseMain: () => void = () => undefined;
638
+ let notifyMainBlocked: () => void = () => undefined;
639
+ const mainReleased = new Promise<void>((resolve) => {
640
+ releaseMain = resolve;
641
+ });
642
+ const mainBlocked = new Promise<void>((resolve) => {
643
+ notifyMainBlocked = resolve;
644
+ });
645
+ session.bus.on("metric.conversation", (pkt) => {
646
+ metrics.push(pkt as ConversationMetricPacket);
647
+ });
648
+ session.bus.on("record.assistant_audio", (pkt) => {
649
+ recording.push(pkt as RecordAssistantAudioPacket);
650
+ });
651
+ session.bus.on("user.audio_received", async () => {
652
+ notifyMainBlocked();
653
+ await mainReleased;
654
+ });
655
+ const server = registerServer(await createTelnyxMediaStreamServer({
656
+ port: 0,
657
+ outputSampleRateHz: 16000,
658
+ outboundFrameDurationMs: 250,
659
+ maxQueuedOutputAudioMs: 30_000,
660
+ createSession: () => session,
661
+ }));
662
+ const address = server.address();
663
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
664
+
665
+ const client = await openSocket(telnyxUrl(address.port));
666
+ const messages: any[] = [];
667
+ client.on("message", (data, isBinary) => {
668
+ if (!isBinary) messages.push(JSON.parse(data.toString()));
669
+ });
670
+ client.send(JSON.stringify(telnyxStart()));
671
+ await new Promise((resolve) => setTimeout(resolve, 20));
672
+
673
+ try {
674
+ const firstMedia = readJsonMatching(client, (message) => message.event === "media");
675
+ session.bus.push(Route.Main, {
676
+ kind: "tts.audio",
677
+ contextId: "telnyx-call-control-test",
678
+ timestampMs: Date.now(),
679
+ audio: pcm16SamplesToBytes(new Int16Array(16000)),
680
+ sampleRateHz: 16000,
681
+ });
682
+ await firstMedia;
683
+ expect(messages.filter((message) => message.event === "media")).toHaveLength(1);
684
+
685
+ client.send(JSON.stringify({
686
+ event: "media",
687
+ stream_id: "telnyx-stream",
688
+ media: {
689
+ payload: Buffer.from(encodePcm16ToMuLaw(new Int16Array([0, 1000, -1000, 3000]))).toString("base64"),
690
+ },
691
+ }));
692
+ await mainBlocked;
693
+
694
+ client.send(JSON.stringify({ event: "stop", stream_id: "telnyx-stream" }));
695
+ await new Promise((resolve) => setTimeout(resolve, 60));
696
+
697
+ expect(messages.filter((message) => message.event === "media")).toHaveLength(1);
698
+ expect(messages.filter((message) => message.event === "mark")).toHaveLength(0);
699
+ expect(metrics).toContainEqual(expect.objectContaining({
700
+ name: "telnyx.stop_playout_cleared_ms",
701
+ value: expect.stringMatching(/^[1-9]\d*$/),
702
+ }));
703
+ expect(recording).toContainEqual(expect.objectContaining({
704
+ contextId: "telnyx-call-control-test",
705
+ truncate: true,
706
+ }));
707
+ } finally {
708
+ releaseMain();
709
+ client.close();
710
+ await server.close();
711
+ }
712
+ });
713
+
714
+ it("truncates queued Telnyx playout when the websocket disconnects without stop", async () => {
715
+ const session = new VoiceAgentSession({ plugins: {} });
716
+ const metrics: ConversationMetricPacket[] = [];
717
+ const recording: RecordAssistantAudioPacket[] = [];
718
+ session.bus.on("metric.conversation", (pkt) => {
719
+ metrics.push(pkt as ConversationMetricPacket);
720
+ });
721
+ session.bus.on("record.assistant_audio", (pkt) => {
722
+ recording.push(pkt as RecordAssistantAudioPacket);
723
+ });
724
+ const server = registerServer(await createTelnyxMediaStreamServer({
725
+ port: 0,
726
+ outputSampleRateHz: 16000,
727
+ outboundFrameDurationMs: 250,
728
+ maxQueuedOutputAudioMs: 30_000,
729
+ createSession: () => session,
730
+ }));
731
+ const address = server.address();
732
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
733
+ const client = await openSocket(telnyxUrl(address.port));
734
+ client.send(JSON.stringify(telnyxStart()));
735
+ await new Promise((resolve) => setTimeout(resolve, 20));
736
+
737
+ const firstMedia = readJsonMatching(client, (message) => message.event === "media");
738
+ session.bus.push(Route.Main, {
739
+ kind: "tts.audio",
740
+ contextId: "telnyx-call-control-test",
741
+ timestampMs: Date.now(),
742
+ audio: pcm16SamplesToBytes(new Int16Array(16000)),
743
+ sampleRateHz: 16000,
744
+ });
745
+ await firstMedia;
746
+ client.terminate();
747
+ await new Promise((resolve) => setTimeout(resolve, 60));
748
+
749
+ expect(metrics).toContainEqual(expect.objectContaining({
750
+ name: "telnyx.disconnect_playout_cleared_ms",
751
+ value: expect.stringMatching(/^[1-9]\d*$/),
752
+ }));
753
+ expect(recording).toContainEqual(expect.objectContaining({ truncate: true }));
754
+ await server.close();
755
+ });
756
+
757
+ it("encodes assistant PCM16 into default PCMU Telnyx media, marks playback, and clears on interruption", async () => {
758
+ const session = new VoiceAgentSession({ plugins: {} });
759
+ const server = registerServer(await createTelnyxMediaStreamServer({
760
+ port: 0,
761
+ outputSampleRateHz: 16000,
762
+ createSession: () => session,
763
+ }));
764
+ const address = server.address();
765
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
766
+
767
+ const client = await openSocket(telnyxUrl(address.port));
768
+ client.send(JSON.stringify(telnyxStart()));
769
+ await new Promise((resolve) => setTimeout(resolve, 20));
770
+
771
+ const mediaMessage = readJsonMatching(client, (message) => message.event === "media");
772
+ const markMessage = readJsonMatching(client, (message) => message.event === "mark");
773
+ const clearMessage = readJsonMatching(client, (message) => message.event === "clear");
774
+ const samples16k = new Int16Array(320);
775
+ for (let i = 0; i < samples16k.length; i += 1) samples16k[i] = i % 2 === 0 ? 1200 : -1200;
776
+
777
+ session.bus.push(Route.Main, {
778
+ kind: "tts.audio",
779
+ contextId: "telnyx-call-control-test",
780
+ timestampMs: Date.now(),
781
+ audio: pcm16SamplesToBytes(samples16k),
782
+ sampleRateHz: 16000,
783
+ });
784
+
785
+ const media = await mediaMessage;
786
+ expect(media).toEqual({
787
+ event: "media",
788
+ media: {
789
+ payload: expect.any(String),
790
+ },
791
+ });
792
+ expect(Buffer.from(media.media.payload, "base64")).toHaveLength(160);
793
+ await expect(markMessage).resolves.toEqual({
794
+ event: "mark",
795
+ mark: {
796
+ name: "telnyx-call-control-test:1",
797
+ },
798
+ });
799
+ session.bus.push(Route.Critical, {
800
+ kind: "interrupt.tts",
801
+ contextId: "telnyx-call-control-test",
802
+ timestampMs: Date.now(),
803
+ });
804
+ await expect(clearMessage).resolves.toEqual({
805
+ event: "clear",
806
+ });
807
+
808
+ client.close();
809
+ await server.close();
810
+ });
811
+
812
+ it("emits tts.playout_progress with completion after the paced audio drains", async () => {
813
+ const session = new VoiceAgentSession({ plugins: {} });
814
+ const server = registerServer(await createTelnyxMediaStreamServer({
815
+ port: 0,
816
+ outputSampleRateHz: 16000,
817
+ createSession: () => session,
818
+ }));
819
+ const address = server.address();
820
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
821
+
822
+ const progress: Array<{ playedOutMs: number; complete: boolean }> = [];
823
+ session.bus.on("tts.playout_progress", (pkt) => {
824
+ const p = pkt as TextToSpeechPlayoutProgressPacket;
825
+ if (p.contextId === "telnyx-playout") progress.push({ playedOutMs: p.playedOutMs, complete: p.complete });
826
+ });
827
+
828
+ const client = await openSocket(telnyxUrl(address.port));
829
+ client.send(JSON.stringify(telnyxStart()));
830
+ await new Promise((resolve) => setTimeout(resolve, 20));
831
+
832
+ // 200ms of audio @ 16 kHz = 10 paced frames of 20ms.
833
+ const samples = new Int16Array(3200);
834
+ for (let i = 0; i < samples.length; i += 1) samples[i] = i % 2 === 0 ? 1200 : -1200;
835
+ session.bus.push(Route.Main, {
836
+ kind: "tts.audio",
837
+ contextId: "telnyx-playout",
838
+ timestampMs: Date.now(),
839
+ audio: pcm16SamplesToBytes(samples),
840
+ sampleRateHz: 16000,
841
+ });
842
+ session.bus.push(Route.Main, {
843
+ kind: "tts.end",
844
+ contextId: "telnyx-playout",
845
+ timestampMs: Date.now(),
846
+ });
847
+ // Wait until the paced drain actually reports completion (deterministic under
848
+ // suite load) instead of a fixed sleep that can slip past its real-time margin.
849
+ await waitForCondition(() => progress.at(-1)?.complete === true);
850
+
851
+ const last = progress.at(-1);
852
+ expect(last?.complete).toBe(true);
853
+ expect(last?.playedOutMs).toBeGreaterThanOrEqual(180);
854
+
855
+ client.close();
856
+ await server.close();
857
+ });
858
+
859
+ it("attributes the final paced frame to its contextId (playout clock not short by one frame)", async () => {
860
+ const session = new VoiceAgentSession({ plugins: {} });
861
+ const server = registerServer(await createTelnyxMediaStreamServer({
862
+ port: 0,
863
+ outputSampleRateHz: 16000,
864
+ createSession: () => session,
865
+ }));
866
+ const address = server.address();
867
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
868
+
869
+ const progress: Array<{ playedOutMs: number; complete: boolean }> = [];
870
+ session.bus.on("tts.playout_progress", (pkt) => {
871
+ const p = pkt as TextToSpeechPlayoutProgressPacket;
872
+ if (p.contextId === "telnyx-final-frame") progress.push({ playedOutMs: p.playedOutMs, complete: p.complete });
873
+ });
874
+
875
+ const client = await openSocket(telnyxUrl(address.port));
876
+ client.send(JSON.stringify(telnyxStart()));
877
+ await new Promise((resolve) => setTimeout(resolve, 20));
878
+
879
+ // 200ms @ 16 kHz = exactly 10 paced frames of 20ms; the playout clock must count all 10.
880
+ const samples = new Int16Array(3200);
881
+ for (let i = 0; i < samples.length; i += 1) samples[i] = i % 2 === 0 ? 1200 : -1200;
882
+ session.bus.push(Route.Main, {
883
+ kind: "tts.audio",
884
+ contextId: "telnyx-final-frame",
885
+ timestampMs: Date.now(),
886
+ audio: pcm16SamplesToBytes(samples),
887
+ sampleRateHz: 16000,
888
+ });
889
+ session.bus.push(Route.Main, {
890
+ kind: "tts.end",
891
+ contextId: "telnyx-final-frame",
892
+ timestampMs: Date.now(),
893
+ });
894
+ await waitForCondition(() => progress.at(-1)?.complete === true);
895
+
896
+ // BUG: the final frame was rebuilt without contextId, so its 20ms was dropped (reported 180, not 200).
897
+ expect(progress.at(-1)?.playedOutMs).toBe(200);
898
+
899
+ client.close();
900
+ await server.close();
901
+ });
902
+
903
+ it("uses configured L16 bidirectional output rather than assuming the inbound codec", async () => {
904
+ const session = new VoiceAgentSession({ plugins: {} });
905
+ const server = registerServer(await createTelnyxMediaStreamServer({
906
+ port: 0,
907
+ outputSampleRateHz: 16000,
908
+ bidirectionalCodec: "L16",
909
+ createSession: () => session,
910
+ }));
911
+ const address = server.address();
912
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
913
+
914
+ const client = await openSocket(telnyxUrl(address.port));
915
+ client.send(JSON.stringify(telnyxStart("PCMU", 8000)));
916
+ await new Promise((resolve) => setTimeout(resolve, 20));
917
+ const mediaMessage = readJsonMatching(client, (message) => message.event === "media");
918
+ const samples = new Int16Array(320);
919
+ samples[0] = 0x1234;
920
+
921
+ session.bus.push(Route.Main, {
922
+ kind: "tts.audio",
923
+ contextId: "telnyx-call-control-test",
924
+ timestampMs: Date.now(),
925
+ audio: pcm16SamplesToBytes(samples),
926
+ sampleRateHz: 16000,
927
+ });
928
+
929
+ const media = await mediaMessage;
930
+ const bytes = Buffer.from(media.media.payload, "base64");
931
+ expect(bytes).toHaveLength(640);
932
+ expect(bytes.subarray(0, 2)).toEqual(Buffer.from([0x12, 0x34]));
933
+
934
+ client.close();
935
+ await server.close();
936
+ });
937
+
938
+ it("paces outbound media and clears locally queued frames on interruption", async () => {
939
+ const session = new VoiceAgentSession({ plugins: {} });
940
+ const server = registerServer(await createTelnyxMediaStreamServer({
941
+ port: 0,
942
+ outputSampleRateHz: 16000,
943
+ outboundFrameDurationMs: 20,
944
+ createSession: () => session,
945
+ }));
946
+ const address = server.address();
947
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
948
+ const client = await openSocket(telnyxUrl(address.port));
949
+ const messages: any[] = [];
950
+ client.on("message", (data, isBinary) => {
951
+ if (!isBinary) messages.push(JSON.parse(data.toString()));
952
+ });
953
+ client.send(JSON.stringify(telnyxStart()));
954
+ await new Promise((resolve) => setTimeout(resolve, 20));
955
+
956
+ session.bus.push(Route.Main, {
957
+ kind: "tts.audio",
958
+ contextId: "telnyx-call-control-test",
959
+ timestampMs: Date.now(),
960
+ audio: pcm16SamplesToBytes(new Int16Array(1280)),
961
+ sampleRateHz: 16000,
962
+ });
963
+ await new Promise((resolve) => setTimeout(resolve, 10));
964
+ expect(messages.filter((message) => message.event === "media")).toHaveLength(1);
965
+ expect(messages.filter((message) => message.event === "mark")).toHaveLength(0);
966
+
967
+ session.bus.push(Route.Critical, {
968
+ kind: "interrupt.tts",
969
+ contextId: "telnyx-call-control-test",
970
+ timestampMs: Date.now(),
971
+ });
972
+ await new Promise((resolve) => setTimeout(resolve, 60));
973
+ expect(messages.filter((message) => message.event === "media")).toHaveLength(1);
974
+ expect(messages.filter((message) => message.event === "mark")).toHaveLength(0);
975
+ expect(messages.filter((message) => message.event === "clear")).toHaveLength(1);
976
+
977
+ session.bus.push(Route.Main, {
978
+ kind: "tts.audio",
979
+ contextId: "telnyx-call-control-test",
980
+ timestampMs: Date.now(),
981
+ audio: pcm16SamplesToBytes(new Int16Array(320)),
982
+ sampleRateHz: 16000,
983
+ });
984
+ session.bus.push(Route.Main, {
985
+ kind: "tts.end",
986
+ contextId: "telnyx-call-control-test",
987
+ timestampMs: Date.now(),
988
+ });
989
+ await new Promise((resolve) => setTimeout(resolve, 60));
990
+
991
+ expect(messages.filter((message) => message.event === "media")).toHaveLength(1);
992
+ expect(messages.filter((message) => message.event === "mark")).toHaveLength(0);
993
+
994
+ client.close();
995
+ await server.close();
996
+ });
997
+
998
+ it("records Telnyx mark callbacks as playback acknowledgement metrics", async () => {
999
+ const session = new VoiceAgentSession({ plugins: {} });
1000
+ const metrics: ConversationMetricPacket[] = [];
1001
+ session.bus.on("metric.conversation", (pkt) => {
1002
+ metrics.push(pkt as ConversationMetricPacket);
1003
+ });
1004
+
1005
+ const server = registerServer(await createTelnyxMediaStreamServer({
1006
+ port: 0,
1007
+ createSession: () => session,
1008
+ }));
1009
+ const address = server.address();
1010
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1011
+
1012
+ const client = await openSocket(telnyxUrl(address.port));
1013
+ client.send(JSON.stringify(telnyxStart()));
1014
+ await new Promise((resolve) => setTimeout(resolve, 20));
1015
+
1016
+ client.send(JSON.stringify({
1017
+ event: "mark",
1018
+ stream_id: "telnyx-stream",
1019
+ mark: {
1020
+ name: "telnyx-call-control-test:1",
1021
+ },
1022
+ }));
1023
+ await new Promise((resolve) => setTimeout(resolve, 20));
1024
+
1025
+ expect(metrics).toEqual([
1026
+ expect.objectContaining({
1027
+ kind: "metric.conversation",
1028
+ contextId: "telnyx-call-control-test",
1029
+ name: "telnyx.mark_received",
1030
+ value: "telnyx-call-control-test:1",
1031
+ }),
1032
+ ]);
1033
+
1034
+ client.close();
1035
+ await server.close();
1036
+ });
1037
+
1038
+ it("sends the terminal Telnyx playback mark after pending playback marks are acknowledged", async () => {
1039
+ const session = new VoiceAgentSession({ plugins: {} });
1040
+ const server = registerServer(await createTelnyxMediaStreamServer({
1041
+ port: 0,
1042
+ outputSampleRateHz: 16000,
1043
+ createSession: () => session,
1044
+ }));
1045
+ const address = server.address();
1046
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1047
+
1048
+ const client = await openSocket(telnyxUrl(address.port));
1049
+ client.send(JSON.stringify(telnyxStart()));
1050
+ await new Promise((resolve) => setTimeout(resolve, 20));
1051
+
1052
+ const playbackMark = readJsonMatching(client, (message) => message.event === "mark" && message.mark?.name === "telnyx-call-control-test:1");
1053
+ session.bus.push(Route.Main, {
1054
+ kind: "tts.audio",
1055
+ contextId: "telnyx-call-control-test",
1056
+ timestampMs: Date.now(),
1057
+ audio: pcm16SamplesToBytes(new Int16Array(320)),
1058
+ sampleRateHz: 16000,
1059
+ });
1060
+ const mark = await playbackMark;
1061
+
1062
+ const endMark = readJsonMatching(client, (message) => message.event === "mark" && message.mark?.name === "telnyx-call-control-test:end");
1063
+ session.bus.push(Route.Main, {
1064
+ kind: "tts.end",
1065
+ contextId: "telnyx-call-control-test",
1066
+ timestampMs: Date.now(),
1067
+ });
1068
+
1069
+ await expect(Promise.race([
1070
+ endMark.then(() => "end"),
1071
+ new Promise((resolve) => setTimeout(() => resolve("pending"), 30)),
1072
+ ])).resolves.toBe("pending");
1073
+
1074
+ client.send(JSON.stringify({ event: "mark", stream_id: "telnyx-stream", mark: { name: mark.mark.name } }));
1075
+ await expect(endMark).resolves.toEqual({
1076
+ event: "mark",
1077
+ mark: {
1078
+ name: "telnyx-call-control-test:end",
1079
+ },
1080
+ });
1081
+
1082
+ client.close();
1083
+ await server.close();
1084
+ });
1085
+
1086
+ it("rejects malformed Telnyx base64 media payloads before forwarding audio", async () => {
1087
+ const session = new VoiceAgentSession({ plugins: {} });
1088
+ const received: UserAudioReceivedPacket[] = [];
1089
+ session.bus.on("user.audio_received", (pkt) => {
1090
+ received.push(pkt as UserAudioReceivedPacket);
1091
+ });
1092
+
1093
+ const server = registerServer(await createTelnyxMediaStreamServer({
1094
+ port: 0,
1095
+ createSession: () => session,
1096
+ }));
1097
+ const address = server.address();
1098
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1099
+
1100
+ const client = await openSocket(telnyxUrl(address.port));
1101
+ const errorMessage = readJsonMatching(client, (message) => message.event === "error");
1102
+ client.send(JSON.stringify(telnyxStart()));
1103
+ client.send(JSON.stringify({
1104
+ event: "media",
1105
+ stream_id: "telnyx-stream",
1106
+ media: {
1107
+ payload: "not base64",
1108
+ },
1109
+ }));
1110
+
1111
+ await expect(errorMessage).resolves.toMatchObject({
1112
+ event: "error",
1113
+ payload: {
1114
+ title: "syrinx_transport_error",
1115
+ detail: "media.payload must be valid base64",
1116
+ },
1117
+ });
1118
+ expect(received).toEqual([]);
1119
+
1120
+ client.close();
1121
+ await server.close();
1122
+ });
1123
+
1124
+ it("rejects malformed Telnyx JSON media envelopes before forwarding audio", async () => {
1125
+ const session = new VoiceAgentSession({ plugins: {} });
1126
+ const received: UserAudioReceivedPacket[] = [];
1127
+ session.bus.on("user.audio_received", (pkt) => {
1128
+ received.push(pkt as UserAudioReceivedPacket);
1129
+ });
1130
+
1131
+ const server = registerServer(await createTelnyxMediaStreamServer({
1132
+ port: 0,
1133
+ createSession: () => session,
1134
+ }));
1135
+ const address = server.address();
1136
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1137
+
1138
+ const client = await openSocket(telnyxUrl(address.port));
1139
+ const errorMessage = readJsonMatching(client, (message) => message.event === "error");
1140
+ client.send(JSON.stringify(telnyxStart()));
1141
+ client.send(JSON.stringify({
1142
+ event: "media",
1143
+ stream_id: "telnyx-stream",
1144
+ media: "not an object",
1145
+ }));
1146
+
1147
+ await expect(errorMessage).resolves.toMatchObject({
1148
+ event: "error",
1149
+ payload: {
1150
+ title: "syrinx_transport_error",
1151
+ detail: "Telnyx media must be a JSON object",
1152
+ },
1153
+ });
1154
+ expect(received).toEqual([]);
1155
+
1156
+ client.close();
1157
+ await server.close();
1158
+ });
1159
+
1160
+ it("sends heartbeat pings to Telnyx websocket peers", async () => {
1161
+ const session = new VoiceAgentSession({ plugins: {} });
1162
+ const server = registerServer(await createTelnyxMediaStreamServer({
1163
+ port: 0,
1164
+ heartbeatIntervalMs: 10,
1165
+ createSession: () => session,
1166
+ }));
1167
+ const address = server.address();
1168
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1169
+
1170
+ const client = await openSocket(telnyxUrl(address.port));
1171
+ const ping = new Promise<void>((resolve) => {
1172
+ client.once("ping", () => resolve());
1173
+ });
1174
+
1175
+ await expect(ping).resolves.toBeUndefined();
1176
+
1177
+ client.close();
1178
+ await server.close();
1179
+ });
1180
+
1181
+ it("closes Telnyx websocket sessions that exceed maxSessionDurationMs", async () => {
1182
+ const session = new VoiceAgentSession({ plugins: {} });
1183
+ const server = registerServer(await createTelnyxMediaStreamServer({
1184
+ port: 0,
1185
+ maxSessionDurationMs: 10,
1186
+ createSession: () => session,
1187
+ }));
1188
+ const address = server.address();
1189
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1190
+
1191
+ const client = await openSocket(telnyxUrl(address.port));
1192
+ const closed = new Promise<{ code: number; reason: string }>((resolve) => {
1193
+ client.once("close", (code, reason) => {
1194
+ resolve({ code, reason: reason.toString() });
1195
+ });
1196
+ });
1197
+
1198
+ await expect(closed).resolves.toEqual({
1199
+ code: 1000,
1200
+ reason: "websocket max session duration exceeded",
1201
+ });
1202
+
1203
+ await server.close();
1204
+ });
1205
+
1206
+ it("closes oversized inbound Telnyx websocket messages before parsing", async () => {
1207
+ const session = new VoiceAgentSession({ plugins: {} });
1208
+ const server = registerServer(await createTelnyxMediaStreamServer({
1209
+ port: 0,
1210
+ maxInboundMessageBytes: 8,
1211
+ createSession: () => session,
1212
+ }));
1213
+ const address = server.address();
1214
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1215
+
1216
+ const client = await openSocket(telnyxUrl(address.port));
1217
+ const closed = new Promise<{ code: number; reason: string }>((resolve) => {
1218
+ client.once("close", (code, reason) => {
1219
+ resolve({ code, reason: reason.toString() });
1220
+ });
1221
+ });
1222
+
1223
+ client.send(JSON.stringify(telnyxStart()));
1224
+
1225
+ await expect(closed).resolves.toEqual({
1226
+ code: 1009,
1227
+ reason: "websocket message too large",
1228
+ });
1229
+
1230
+ await server.close();
1231
+ });
1232
+
1233
+ it("closes slow Telnyx consumers before outbound media buffers grow unbounded", async () => {
1234
+ const session = new VoiceAgentSession({ plugins: {} });
1235
+ const metrics: ConversationMetricPacket[] = [];
1236
+ session.bus.on("metric.conversation", (pkt) => {
1237
+ metrics.push(pkt as ConversationMetricPacket);
1238
+ });
1239
+ const server = registerServer(await createTelnyxMediaStreamServer({
1240
+ port: 0,
1241
+ maxBufferedAmountBytes: 1,
1242
+ createSession: () => session,
1243
+ }));
1244
+ const address = server.address();
1245
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1246
+
1247
+ const client = await openSocket(telnyxUrl(address.port));
1248
+ client.send(JSON.stringify(telnyxStart()));
1249
+ await waitForCondition(() => server.wsServer.clients.size > 0);
1250
+
1251
+ const serverSocket = [...server.wsServer.clients][0];
1252
+ if (!serverSocket) throw new Error("Expected server websocket");
1253
+ Object.defineProperty(serverSocket, "bufferedAmount", { configurable: true, value: 2 });
1254
+
1255
+ let isClosed = false;
1256
+ const closed = new Promise<{ code: number; reason: string }>((resolve) => {
1257
+ client.once("close", (code, reason) => {
1258
+ isClosed = true;
1259
+ resolve({ code, reason: reason.toString() });
1260
+ });
1261
+ });
1262
+ // Re-push until the outbound pipeline (which finishes wiring a few async ticks
1263
+ // after connect — slower under full-suite CPU load) trips backpressure. Pushes
1264
+ // before wiring are dropped (no streamId yet); the first that lands trips the
1265
+ // 1013 close; pushes after the close are no-ops on the closed socket — so there
1266
+ // is still exactly one backpressure event and the discarded-ms metric stays "20".
1267
+ await waitForCondition(() => {
1268
+ if (isClosed) return true;
1269
+ session.bus.push(Route.Main, {
1270
+ kind: "tts.audio",
1271
+ contextId: "telnyx-call-control-test",
1272
+ timestampMs: Date.now(),
1273
+ audio: pcm16SamplesToBytes(new Int16Array(320)),
1274
+ sampleRateHz: 16000,
1275
+ });
1276
+ return false;
1277
+ });
1278
+
1279
+ await expect(closed).resolves.toEqual({
1280
+ code: 1013,
1281
+ reason: "websocket send buffer exceeded",
1282
+ });
1283
+ await waitForCondition(() =>
1284
+ metrics.some((m) => (m as ConversationMetricPacket).name === "telnyx.send_buffer_playout_cleared_ms"),
1285
+ );
1286
+ expect(metrics).not.toContainEqual(expect.objectContaining({
1287
+ name: "telnyx.mark_sent",
1288
+ }));
1289
+ expect(metrics).toContainEqual(expect.objectContaining({
1290
+ name: "telnyx.send_buffer_playout_cleared_ms",
1291
+ value: "20",
1292
+ }));
1293
+
1294
+ await server.close();
1295
+ });
1296
+
1297
+ it("drops Telnyx DTMF before start with a metric and no dtmf.received packet", async () => {
1298
+ const session = new VoiceAgentSession({ plugins: {} });
1299
+ const dtmfReceived: unknown[] = [];
1300
+ const metrics: ConversationMetricPacket[] = [];
1301
+ session.bus.on("dtmf.received", (pkt) => { dtmfReceived.push(pkt); });
1302
+ session.bus.on("metric.conversation", (pkt) => {
1303
+ metrics.push(pkt as ConversationMetricPacket);
1304
+ });
1305
+
1306
+ const server = registerServer(await createTelnyxMediaStreamServer({
1307
+ port: 0,
1308
+ createSession: () => session,
1309
+ }));
1310
+ const address = server.address();
1311
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1312
+
1313
+ const client = await openSocket(telnyxUrl(address.port));
1314
+ client.send(JSON.stringify({
1315
+ event: "dtmf",
1316
+ stream_id: "telnyx-stream",
1317
+ dtmf: { digit: "5" },
1318
+ }));
1319
+ await new Promise((resolve) => setTimeout(resolve, 20));
1320
+
1321
+ expect(dtmfReceived).toEqual([]);
1322
+ expect(metrics).toContainEqual(expect.objectContaining({
1323
+ name: "telnyx.dtmf.before_start",
1324
+ value: "5",
1325
+ }));
1326
+
1327
+ client.close();
1328
+ await server.close();
1329
+ });
1330
+
1331
+ it("test:dtmf_typed_per_carrier emits dtmf.received for Telnyx DTMF", async () => {
1332
+ const session = new VoiceAgentSession({ plugins: {} });
1333
+ const dtmfReceived: Array<{ kind: string; digit: string; provider: string; rawDigit: string; contextId: string }> = [];
1334
+ session.bus.on("dtmf.received", (pkt) => {
1335
+ dtmfReceived.push(pkt as unknown as { kind: string; digit: string; provider: string; rawDigit: string; contextId: string });
1336
+ });
1337
+
1338
+ const server = registerServer(await createTelnyxMediaStreamServer({
1339
+ port: 0,
1340
+ createSession: () => session,
1341
+ }));
1342
+ const address = server.address();
1343
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1344
+
1345
+ const client = await openSocket(telnyxUrl(address.port));
1346
+ client.send(JSON.stringify(telnyxStart()));
1347
+ client.send(JSON.stringify({
1348
+ event: "dtmf",
1349
+ stream_id: "telnyx-stream",
1350
+ dtmf: { digit: "5" },
1351
+ }));
1352
+ await waitForCondition(() => dtmfReceived.length === 1);
1353
+
1354
+ expect(dtmfReceived).toEqual([
1355
+ expect.objectContaining({
1356
+ kind: "dtmf.received",
1357
+ digit: "5",
1358
+ provider: "telnyx",
1359
+ rawDigit: "5",
1360
+ contextId: "telnyx-call-control-test",
1361
+ }),
1362
+ ]);
1363
+
1364
+ client.close();
1365
+ await server.close();
1366
+ });
1367
+
1368
+ it("test:dtmf_bypasses_speech_path: Telnyx DTMF during playout emits no audio, VAD, STT, or interrupt packets", async () => {
1369
+ const session = new VoiceAgentSession({ plugins: {} });
1370
+ const dtmfReceived: unknown[] = [];
1371
+ const speechPath: unknown[] = [];
1372
+ const speechKinds = [
1373
+ "user.audio_received",
1374
+ "stt.audio",
1375
+ "vad.audio",
1376
+ "vad.speech_started",
1377
+ "vad.speech_ended",
1378
+ "vad.speech_activity",
1379
+ "interrupt.detected",
1380
+ "interrupt.tts",
1381
+ "interrupt.llm",
1382
+ "interrupt.stt",
1383
+ ] as const;
1384
+ session.bus.on("dtmf.received", (pkt) => { dtmfReceived.push(pkt); });
1385
+ for (const kind of speechKinds) {
1386
+ session.bus.on(kind, (pkt) => { speechPath.push(pkt); });
1387
+ }
1388
+
1389
+ const server = registerServer(await createTelnyxMediaStreamServer({
1390
+ port: 0,
1391
+ outputSampleRateHz: 16000,
1392
+ createSession: () => session,
1393
+ }));
1394
+ const address = server.address();
1395
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1396
+
1397
+ const client = await openSocket(telnyxUrl(address.port));
1398
+ client.send(JSON.stringify(telnyxStart()));
1399
+ await new Promise((resolve) => setTimeout(resolve, 20));
1400
+
1401
+ session.bus.push(Route.Main, {
1402
+ kind: "tts.audio",
1403
+ contextId: "telnyx-call-control-test",
1404
+ timestampMs: Date.now(),
1405
+ audio: pcm16SamplesToBytes(new Int16Array(320)),
1406
+ sampleRateHz: 16000,
1407
+ });
1408
+ client.send(JSON.stringify({
1409
+ event: "dtmf",
1410
+ stream_id: "telnyx-stream",
1411
+ dtmf: { digit: "*" },
1412
+ }));
1413
+ await waitForCondition(() => dtmfReceived.length === 1);
1414
+
1415
+ expect(dtmfReceived).toEqual([
1416
+ expect.objectContaining({
1417
+ kind: "dtmf.received",
1418
+ digit: "*",
1419
+ provider: "telnyx",
1420
+ rawDigit: "*",
1421
+ }),
1422
+ ]);
1423
+ expect(speechPath).toEqual([]);
1424
+
1425
+ client.close();
1426
+ await server.close();
1427
+ });
1428
+
1429
+ it("emits dtmf.unparsed for invalid Telnyx DTMF digits", async () => {
1430
+ const session = new VoiceAgentSession({ plugins: {} });
1431
+ const metrics: ConversationMetricPacket[] = [];
1432
+ const dtmfReceived: unknown[] = [];
1433
+ session.bus.on("metric.conversation", (pkt) => {
1434
+ metrics.push(pkt as ConversationMetricPacket);
1435
+ });
1436
+ session.bus.on("dtmf.received", (pkt) => { dtmfReceived.push(pkt); });
1437
+
1438
+ const server = registerServer(await createTelnyxMediaStreamServer({
1439
+ port: 0,
1440
+ createSession: () => session,
1441
+ }));
1442
+ const address = server.address();
1443
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1444
+
1445
+ const client = await openSocket(telnyxUrl(address.port));
1446
+ client.send(JSON.stringify(telnyxStart()));
1447
+ client.send(JSON.stringify({
1448
+ event: "dtmf",
1449
+ stream_id: "telnyx-stream",
1450
+ dtmf: { digit: "A" },
1451
+ }));
1452
+ await waitForCondition(() => metrics.some((metric) => metric.name === "dtmf.unparsed"));
1453
+
1454
+ expect(metrics).toContainEqual(expect.objectContaining({
1455
+ kind: "metric.conversation",
1456
+ name: "dtmf.unparsed",
1457
+ value: "A",
1458
+ contextId: "telnyx-call-control-test",
1459
+ }));
1460
+ expect(dtmfReceived).toEqual([]);
1461
+
1462
+ client.close();
1463
+ await server.close();
1464
+ });
1465
+ });
1466
+
1467
+
1468
+ describe("validateTelnyxStart — PCMA + G722", () => {
1469
+ it("accepts PCMA @ 8 kHz and G722 @ 16 kHz", () => {
1470
+ expect(validateTelnyxStart({
1471
+ media_format: { encoding: "PCMA", sample_rate: 8000, channels: 1 },
1472
+ })).toEqual({ codec: "PCMA", sampleRateHz: 8000 });
1473
+ expect(validateTelnyxStart({
1474
+ media_format: { encoding: "G722", sample_rate: 16000, channels: 1 },
1475
+ })).toEqual({ codec: "G722", sampleRateHz: 16000 });
1476
+ });
1477
+
1478
+ it("rejects G722 at 8 kHz and PCMA at 16 kHz", () => {
1479
+ expect(() => validateTelnyxStart({
1480
+ media_format: { encoding: "G722", sample_rate: 8000, channels: 1 },
1481
+ })).toThrow(/G722 sample rate/);
1482
+ expect(() => validateTelnyxStart({
1483
+ media_format: { encoding: "PCMA", sample_rate: 16000, channels: 1 },
1484
+ })).toThrow(/PCMA sample rate/);
1485
+ });
1486
+ });
1487
+
1488
+ describe("Telnyx PCMA / G722 media transcode", () => {
1489
+ it("decodes Telnyx PCMA media frames into engine PCM16", async () => {
1490
+ const session = new VoiceAgentSession({ plugins: {} });
1491
+ const received: UserAudioReceivedPacket[] = [];
1492
+ session.bus.on("user.audio_received", (pkt) => {
1493
+ received.push(pkt as UserAudioReceivedPacket);
1494
+ });
1495
+
1496
+ const server = registerServer(await createTelnyxMediaStreamServer({
1497
+ port: 0,
1498
+ inputSampleRateHz: 16000,
1499
+ createSession: () => session,
1500
+ }));
1501
+ const address = server.address();
1502
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1503
+
1504
+ const client = await openSocket(telnyxUrl(address.port));
1505
+ client.send(JSON.stringify(telnyxStart("PCMA", 8000)));
1506
+
1507
+ const samples8k = new Int16Array(160);
1508
+ for (let i = 0; i < samples8k.length; i += 1) {
1509
+ samples8k[i] = Math.round(4000 * Math.sin((2 * Math.PI * 400 * i) / 8000));
1510
+ }
1511
+ const payload = Buffer.from(encodePcm16ToALaw(samples8k)).toString("base64");
1512
+ client.send(JSON.stringify({
1513
+ event: "media",
1514
+ stream_id: "telnyx-stream",
1515
+ media: { payload, chunk: "1", timestamp: "0" },
1516
+ }));
1517
+
1518
+ await waitForCondition(() => received.length >= 1);
1519
+ expect(received[0]!.audio.byteLength).toBeGreaterThan(0);
1520
+ // 8 kHz → 16 kHz engine: roughly 2× samples
1521
+ expect(received[0]!.sampleRateHz).toBe(16000);
1522
+
1523
+ client.close();
1524
+ await server.close();
1525
+ });
1526
+
1527
+ it("decodes Telnyx G722 media at 16 kHz without downsampling to 8 kHz", async () => {
1528
+ const session = new VoiceAgentSession({ plugins: {} });
1529
+ const received: UserAudioReceivedPacket[] = [];
1530
+ session.bus.on("user.audio_received", (pkt) => {
1531
+ received.push(pkt as UserAudioReceivedPacket);
1532
+ });
1533
+
1534
+ const server = registerServer(await createTelnyxMediaStreamServer({
1535
+ port: 0,
1536
+ inputSampleRateHz: 16000,
1537
+ createSession: () => session,
1538
+ }));
1539
+ const address = server.address();
1540
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1541
+
1542
+ const client = await openSocket(telnyxUrl(address.port));
1543
+ client.send(JSON.stringify(telnyxStart("G722", 16000)));
1544
+
1545
+ const samples16k = new Int16Array(320); // 20 ms @ 16 kHz
1546
+ for (let i = 0; i < samples16k.length; i += 1) {
1547
+ samples16k[i] = Math.round(5000 * Math.sin((2 * Math.PI * 1000 * i) / 16000));
1548
+ }
1549
+ const enc = createG722EncoderState();
1550
+ const g722 = encodeG722(enc, samples16k);
1551
+ const payload = Buffer.from(g722).toString("base64");
1552
+ client.send(JSON.stringify({
1553
+ event: "media",
1554
+ stream_id: "telnyx-stream",
1555
+ media: { payload, chunk: "1", timestamp: "0" },
1556
+ }));
1557
+
1558
+ await waitForCondition(() => received.length >= 1);
1559
+ // Engine stays at 16 kHz — G.722 wideband quality preserved for STT.
1560
+ expect(received[0]!.sampleRateHz).toBe(16000);
1561
+ // ~320 samples * 2 bytes
1562
+ expect(received[0]!.audio.byteLength).toBeGreaterThanOrEqual(600);
1563
+
1564
+ client.close();
1565
+ await server.close();
1566
+ });
1567
+ });
1568
+
1569
+ describe("Telnyx dtmf.send / call.transfer bus wiring", () => {
1570
+ it("dtmf.send builds Telnyx send_dtmf command metric without live HTTP", async () => {
1571
+ const session = new VoiceAgentSession({ plugins: {} });
1572
+ const metrics: ConversationMetricPacket[] = [];
1573
+ session.bus.on("metric.conversation", (pkt) => {
1574
+ metrics.push(pkt as ConversationMetricPacket);
1575
+ });
1576
+
1577
+ const server = registerServer(await createTelnyxMediaStreamServer({
1578
+ port: 0,
1579
+ createSession: () => session,
1580
+ }));
1581
+ const address = server.address();
1582
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1583
+
1584
+ const client = await openSocket(telnyxUrl(address.port));
1585
+ client.send(JSON.stringify(telnyxStart("PCMU", 8000)));
1586
+ // allow start to process
1587
+ await new Promise((r) => setTimeout(r, 50));
1588
+
1589
+ session.bus.push(Route.Critical, dtmfSend("telnyx-call-control-test", Date.now(), "1w2"));
1590
+
1591
+ await waitForCondition(() => metrics.some((m) => m.name === "carrier.dtmf_send.built"));
1592
+ const built = metrics.find((m) => m.name === "carrier.dtmf_send.built");
1593
+ expect(built?.value).toContain("telnyx");
1594
+ expect(built?.value).toContain("1w2");
1595
+ expect(metrics.some((m) => m.name === "carrier.dtmf_send.no_credentials")).toBe(true);
1596
+
1597
+ client.close();
1598
+ await server.close();
1599
+ });
1600
+
1601
+ it("call.transfer warm summary flows into built command metric", async () => {
1602
+ const session = new VoiceAgentSession({ plugins: {} });
1603
+ const metrics: ConversationMetricPacket[] = [];
1604
+ session.bus.on("metric.conversation", (pkt) => {
1605
+ metrics.push(pkt as ConversationMetricPacket);
1606
+ });
1607
+
1608
+ const server = registerServer(await createTelnyxMediaStreamServer({
1609
+ port: 0,
1610
+ createSession: () => session,
1611
+ warmTransferSummarizer: async () => "from summarizer seam",
1612
+ }));
1613
+ const address = server.address();
1614
+ if (!address || typeof address === "string") throw new Error("Expected TCP address");
1615
+
1616
+ const client = await openSocket(telnyxUrl(address.port));
1617
+ client.send(JSON.stringify(telnyxStart("PCMU", 8000)));
1618
+ await new Promise((r) => setTimeout(r, 50));
1619
+
1620
+ session.bus.push(
1621
+ Route.Critical,
1622
+ callTransfer("telnyx-call-control-test", Date.now(), "warm", "+15551234567"),
1623
+ );
1624
+
1625
+ await waitForCondition(() => metrics.some((m) => m.name === "carrier.transfer.built"));
1626
+ const built = metrics.find((m) => m.name === "carrier.transfer.built");
1627
+ expect(built?.value).toContain("from summarizer seam");
1628
+ expect(built?.value).toContain("+15551234567");
1629
+
1630
+ client.close();
1631
+ await server.close();
1632
+ });
1633
+ });