@kodelyth/voice-call 2026.5.42 → 2026.6.1

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 (111) hide show
  1. package/package.json +16 -4
  2. package/api.ts +0 -16
  3. package/cli-metadata.ts +0 -10
  4. package/config-api.ts +0 -12
  5. package/index.test.ts +0 -1075
  6. package/index.ts +0 -863
  7. package/runtime-api.ts +0 -20
  8. package/runtime-entry.ts +0 -1
  9. package/setup-api.ts +0 -47
  10. package/src/allowlist.test.ts +0 -18
  11. package/src/allowlist.ts +0 -19
  12. package/src/cli.test.ts +0 -12
  13. package/src/cli.ts +0 -866
  14. package/src/config-compat.test.ts +0 -130
  15. package/src/config-compat.ts +0 -227
  16. package/src/config.test.ts +0 -542
  17. package/src/config.ts +0 -883
  18. package/src/core-bridge.ts +0 -14
  19. package/src/deep-merge.test.ts +0 -40
  20. package/src/deep-merge.ts +0 -23
  21. package/src/gateway-continue-operation.ts +0 -200
  22. package/src/http-headers.test.ts +0 -16
  23. package/src/http-headers.ts +0 -15
  24. package/src/manager/context.ts +0 -50
  25. package/src/manager/events.test.ts +0 -578
  26. package/src/manager/events.ts +0 -332
  27. package/src/manager/lifecycle.ts +0 -53
  28. package/src/manager/lookup.test.ts +0 -52
  29. package/src/manager/lookup.ts +0 -35
  30. package/src/manager/outbound.test.ts +0 -629
  31. package/src/manager/outbound.ts +0 -508
  32. package/src/manager/state.ts +0 -48
  33. package/src/manager/store.ts +0 -107
  34. package/src/manager/timers.test.ts +0 -127
  35. package/src/manager/timers.ts +0 -113
  36. package/src/manager/twiml.test.ts +0 -13
  37. package/src/manager/twiml.ts +0 -17
  38. package/src/manager.closed-loop.test.ts +0 -259
  39. package/src/manager.inbound-allowlist.test.ts +0 -183
  40. package/src/manager.notify.test.ts +0 -390
  41. package/src/manager.restore.test.ts +0 -310
  42. package/src/manager.test-harness.ts +0 -127
  43. package/src/manager.ts +0 -441
  44. package/src/media-stream.test.ts +0 -953
  45. package/src/media-stream.ts +0 -876
  46. package/src/providers/base.ts +0 -99
  47. package/src/providers/mock.test.ts +0 -86
  48. package/src/providers/mock.ts +0 -185
  49. package/src/providers/plivo.test.ts +0 -93
  50. package/src/providers/plivo.ts +0 -601
  51. package/src/providers/shared/call-status.test.ts +0 -24
  52. package/src/providers/shared/call-status.ts +0 -24
  53. package/src/providers/shared/guarded-json-api.test.ts +0 -127
  54. package/src/providers/shared/guarded-json-api.ts +0 -49
  55. package/src/providers/telnyx.test.ts +0 -489
  56. package/src/providers/telnyx.ts +0 -419
  57. package/src/providers/twilio/api.test.ts +0 -184
  58. package/src/providers/twilio/api.ts +0 -100
  59. package/src/providers/twilio/twiml-policy.test.ts +0 -84
  60. package/src/providers/twilio/twiml-policy.ts +0 -87
  61. package/src/providers/twilio/webhook.ts +0 -34
  62. package/src/providers/twilio.test.ts +0 -607
  63. package/src/providers/twilio.ts +0 -861
  64. package/src/providers/twilio.types.ts +0 -17
  65. package/src/realtime-agent-context.test.ts +0 -101
  66. package/src/realtime-agent-context.ts +0 -149
  67. package/src/realtime-defaults.ts +0 -3
  68. package/src/realtime-fast-context.test.ts +0 -74
  69. package/src/realtime-fast-context.ts +0 -27
  70. package/src/realtime-transcription.runtime.ts +0 -4
  71. package/src/realtime-voice.runtime.ts +0 -5
  72. package/src/response-generator.test.ts +0 -385
  73. package/src/response-generator.ts +0 -348
  74. package/src/response-model.test.ts +0 -71
  75. package/src/response-model.ts +0 -23
  76. package/src/runtime.test.ts +0 -625
  77. package/src/runtime.ts +0 -528
  78. package/src/telephony-audio.test.ts +0 -61
  79. package/src/telephony-audio.ts +0 -12
  80. package/src/telephony-tts.test.ts +0 -196
  81. package/src/telephony-tts.ts +0 -235
  82. package/src/test-fixtures.ts +0 -82
  83. package/src/tts-provider-voice.test.ts +0 -34
  84. package/src/tts-provider-voice.ts +0 -21
  85. package/src/tunnel.test.ts +0 -173
  86. package/src/tunnel.ts +0 -314
  87. package/src/types.ts +0 -311
  88. package/src/utils.test.ts +0 -17
  89. package/src/utils.ts +0 -14
  90. package/src/voice-mapping.test.ts +0 -32
  91. package/src/voice-mapping.ts +0 -65
  92. package/src/webhook/realtime-audio-pacer.test.ts +0 -146
  93. package/src/webhook/realtime-audio-pacer.ts +0 -204
  94. package/src/webhook/realtime-handler.test.ts +0 -1450
  95. package/src/webhook/realtime-handler.ts +0 -1382
  96. package/src/webhook/stale-call-reaper.test.ts +0 -89
  97. package/src/webhook/stale-call-reaper.ts +0 -38
  98. package/src/webhook/stream-frame-adapter.test.ts +0 -187
  99. package/src/webhook/stream-frame-adapter.ts +0 -219
  100. package/src/webhook/tailscale.test.ts +0 -216
  101. package/src/webhook/tailscale.ts +0 -129
  102. package/src/webhook-exposure.test.ts +0 -33
  103. package/src/webhook-exposure.ts +0 -84
  104. package/src/webhook-security.test.ts +0 -813
  105. package/src/webhook-security.ts +0 -982
  106. package/src/webhook.hangup-once.lifecycle.test.ts +0 -179
  107. package/src/webhook.test.ts +0 -1615
  108. package/src/webhook.ts +0 -933
  109. package/src/webhook.types.ts +0 -5
  110. package/src/websocket-test-support.ts +0 -72
  111. package/tsconfig.json +0 -16
@@ -1,542 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import {
3
- VoiceCallConfigSchema,
4
- resolveTwilioAuthToken,
5
- resolveVoiceCallEffectiveConfig,
6
- resolveVoiceCallNumberRouteKey,
7
- resolveVoiceCallSessionKey,
8
- validateProviderConfig,
9
- normalizeVoiceCallConfig,
10
- resolveVoiceCallConfig,
11
- type VoiceCallConfig,
12
- } from "./config.js";
13
- import { createVoiceCallBaseConfig } from "./test-fixtures.js";
14
-
15
- function createBaseConfig(provider: "telnyx" | "twilio" | "plivo" | "mock"): VoiceCallConfig {
16
- return createVoiceCallBaseConfig({ provider });
17
- }
18
-
19
- function envRef(id: string) {
20
- return { source: "env" as const, provider: "default", id };
21
- }
22
-
23
- function requireElevenLabsTtsConfig(config: Pick<VoiceCallConfig, "tts">) {
24
- const tts = config.tts;
25
- const elevenlabs = tts?.providers?.elevenlabs;
26
- if (!elevenlabs || typeof elevenlabs !== "object") {
27
- throw new Error("voice-call config did not preserve nested elevenlabs TTS config");
28
- }
29
- return { tts, elevenlabs };
30
- }
31
-
32
- describe("validateProviderConfig", () => {
33
- const originalEnv = { ...process.env };
34
- const clearProviderEnv = () => {
35
- delete process.env.TWILIO_ACCOUNT_SID;
36
- delete process.env.TWILIO_AUTH_TOKEN;
37
- delete process.env.TWILIO_FROM_NUMBER;
38
- delete process.env.TELNYX_API_KEY;
39
- delete process.env.TELNYX_CONNECTION_ID;
40
- delete process.env.TELNYX_PUBLIC_KEY;
41
- delete process.env.PLIVO_AUTH_ID;
42
- delete process.env.PLIVO_AUTH_TOKEN;
43
- };
44
-
45
- beforeEach(() => {
46
- clearProviderEnv();
47
- });
48
-
49
- afterEach(() => {
50
- // Restore original env
51
- process.env = { ...originalEnv };
52
- });
53
-
54
- describe("provider credential sources", () => {
55
- it("passes validation when credentials come from config or environment", () => {
56
- for (const provider of ["twilio", "telnyx", "plivo"] as const) {
57
- clearProviderEnv();
58
- const fromConfig = createBaseConfig(provider);
59
- if (provider === "twilio") {
60
- fromConfig.twilio = { accountSid: "AC123", authToken: "secret" };
61
- } else if (provider === "telnyx") {
62
- fromConfig.telnyx = {
63
- apiKey: "KEY123",
64
- connectionId: "CONN456",
65
- publicKey: "public-key",
66
- };
67
- } else {
68
- fromConfig.plivo = { authId: "MA123", authToken: "secret" };
69
- }
70
- expect(validateProviderConfig(fromConfig)).toEqual({ valid: true, errors: [] });
71
-
72
- clearProviderEnv();
73
- if (provider === "twilio") {
74
- process.env.TWILIO_ACCOUNT_SID = "AC123";
75
- process.env.TWILIO_AUTH_TOKEN = "secret";
76
- process.env.TWILIO_FROM_NUMBER = "+15550001234";
77
- } else if (provider === "telnyx") {
78
- process.env.TELNYX_API_KEY = "KEY123";
79
- process.env.TELNYX_CONNECTION_ID = "CONN456";
80
- process.env.TELNYX_PUBLIC_KEY = "public-key";
81
- } else {
82
- process.env.PLIVO_AUTH_ID = "MA123";
83
- process.env.PLIVO_AUTH_TOKEN = "secret";
84
- }
85
- const fromEnv = resolveVoiceCallConfig(createBaseConfig(provider));
86
- expect(validateProviderConfig(fromEnv)).toEqual({ valid: true, errors: [] });
87
- }
88
- });
89
- });
90
-
91
- describe("twilio provider", () => {
92
- it("accepts SecretRef-backed auth tokens before runtime resolution", () => {
93
- const config = VoiceCallConfigSchema.parse({
94
- enabled: true,
95
- provider: "twilio",
96
- fromNumber: "+15550001234",
97
- twilio: {
98
- accountSid: "AC123",
99
- authToken: envRef("TWILIO_AUTH_TOKEN"),
100
- },
101
- });
102
-
103
- expect(config.twilio?.authToken).toEqual(envRef("TWILIO_AUTH_TOKEN"));
104
- expect(validateProviderConfig(config)).toEqual({ valid: true, errors: [] });
105
- expect(() => resolveTwilioAuthToken(config)).toThrow(
106
- 'plugins.entries.voice-call.config.twilio.authToken: unresolved SecretRef "env:default:TWILIO_AUTH_TOKEN"',
107
- );
108
- });
109
-
110
- it("passes validation with mixed config and env vars", () => {
111
- process.env.TWILIO_AUTH_TOKEN = "secret";
112
- let config = createBaseConfig("twilio");
113
- config.twilio = { accountSid: "AC123" };
114
- config = resolveVoiceCallConfig(config);
115
-
116
- const result = validateProviderConfig(config);
117
-
118
- expect(result.valid).toBe(true);
119
- expect(result.errors).toStrictEqual([]);
120
- });
121
-
122
- it("resolves the Twilio from number from environment", () => {
123
- process.env.TWILIO_ACCOUNT_SID = "AC123";
124
- process.env.TWILIO_AUTH_TOKEN = "secret";
125
- process.env.TWILIO_FROM_NUMBER = "+15550001234";
126
-
127
- const config = resolveVoiceCallConfig({
128
- ...createBaseConfig("twilio"),
129
- fromNumber: undefined,
130
- });
131
-
132
- expect(config.fromNumber).toBe("+15550001234");
133
- expect(validateProviderConfig(config)).toEqual({ valid: true, errors: [] });
134
- });
135
-
136
- it("fails validation when required twilio credentials are missing", () => {
137
- process.env.TWILIO_AUTH_TOKEN = "secret";
138
- const missingSid = validateProviderConfig(resolveVoiceCallConfig(createBaseConfig("twilio")));
139
- expect(missingSid.valid).toBe(false);
140
- expect(missingSid.errors).toContain(
141
- "plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
142
- );
143
-
144
- delete process.env.TWILIO_AUTH_TOKEN;
145
- process.env.TWILIO_ACCOUNT_SID = "AC123";
146
- const missingToken = validateProviderConfig(
147
- resolveVoiceCallConfig(createBaseConfig("twilio")),
148
- );
149
- expect(missingToken.valid).toBe(false);
150
- expect(missingToken.errors).toContain(
151
- "plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
152
- );
153
- });
154
- });
155
-
156
- describe("telnyx provider", () => {
157
- it("fails validation when apiKey is missing everywhere", () => {
158
- process.env.TELNYX_CONNECTION_ID = "CONN456";
159
- let config = createBaseConfig("telnyx");
160
- config = resolveVoiceCallConfig(config);
161
-
162
- const result = validateProviderConfig(config);
163
-
164
- expect(result.valid).toBe(false);
165
- expect(result.errors).toContain(
166
- "plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)",
167
- );
168
- });
169
-
170
- it("requires a public key unless signature verification is skipped", () => {
171
- const missingPublicKey = createBaseConfig("telnyx");
172
- missingPublicKey.inboundPolicy = "allowlist";
173
- missingPublicKey.telnyx = { apiKey: "KEY123", connectionId: "CONN456" };
174
- const missingPublicKeyResult = validateProviderConfig(missingPublicKey);
175
- expect(missingPublicKeyResult.valid).toBe(false);
176
- expect(missingPublicKeyResult.errors).toContain(
177
- "plugins.entries.voice-call.config.telnyx.publicKey is required (or set TELNYX_PUBLIC_KEY env)",
178
- );
179
-
180
- const withPublicKey = createBaseConfig("telnyx");
181
- withPublicKey.inboundPolicy = "allowlist";
182
- withPublicKey.telnyx = {
183
- apiKey: "KEY123",
184
- connectionId: "CONN456",
185
- publicKey: "public-key",
186
- };
187
- expect(validateProviderConfig(withPublicKey)).toEqual({ valid: true, errors: [] });
188
-
189
- const skippedVerification = createBaseConfig("telnyx");
190
- skippedVerification.skipSignatureVerification = true;
191
- skippedVerification.telnyx = { apiKey: "KEY123", connectionId: "CONN456" };
192
- expect(validateProviderConfig(skippedVerification)).toEqual({
193
- valid: true,
194
- errors: [],
195
- });
196
- });
197
- });
198
-
199
- describe("plivo provider", () => {
200
- it("fails validation when authId is missing everywhere", () => {
201
- process.env.PLIVO_AUTH_TOKEN = "secret";
202
- let config = createBaseConfig("plivo");
203
- config = resolveVoiceCallConfig(config);
204
-
205
- const result = validateProviderConfig(config);
206
-
207
- expect(result.valid).toBe(false);
208
- expect(result.errors).toContain(
209
- "plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)",
210
- );
211
- });
212
- });
213
-
214
- describe("disabled config", () => {
215
- it("skips validation when enabled is false", () => {
216
- const config = createBaseConfig("twilio");
217
- config.enabled = false;
218
-
219
- const result = validateProviderConfig(config);
220
-
221
- expect(result.valid).toBe(true);
222
- expect(result.errors).toStrictEqual([]);
223
- });
224
- });
225
-
226
- describe("realtime config", () => {
227
- it("rejects disabled inbound policy for realtime mode", () => {
228
- const config = createBaseConfig("twilio");
229
- config.realtime.enabled = true;
230
- config.inboundPolicy = "disabled";
231
-
232
- const result = validateProviderConfig(config);
233
-
234
- expect(result.valid).toBe(false);
235
- expect(result.errors).toContain(
236
- 'plugins.entries.voice-call.config.inboundPolicy must not be "disabled" when realtime.enabled is true',
237
- );
238
- });
239
-
240
- it("rejects enabling realtime and streaming together", () => {
241
- const config = createBaseConfig("twilio");
242
- config.realtime.enabled = true;
243
- config.streaming.enabled = true;
244
- config.inboundPolicy = "allowlist";
245
-
246
- const result = validateProviderConfig(config);
247
-
248
- expect(result.valid).toBe(false);
249
- expect(result.errors).toContain(
250
- "plugins.entries.voice-call.config.realtime.enabled and plugins.entries.voice-call.config.streaming.enabled cannot both be true",
251
- );
252
- });
253
-
254
- it("accepts realtime.enabled with provider=telnyx", () => {
255
- const config = createBaseConfig("telnyx");
256
- config.realtime.enabled = true;
257
- config.inboundPolicy = "allowlist";
258
-
259
- const result = validateProviderConfig(config);
260
-
261
- expect(result.errors).not.toContain(
262
- 'plugins.entries.voice-call.config.provider must be "twilio" or "telnyx" when realtime.enabled is true',
263
- );
264
- });
265
-
266
- it("rejects realtime.enabled with providers that do not support it yet", () => {
267
- const config = createBaseConfig("plivo");
268
- config.realtime.enabled = true;
269
- config.inboundPolicy = "allowlist";
270
-
271
- const result = validateProviderConfig(config);
272
-
273
- expect(result.valid).toBe(false);
274
- expect(result.errors).toContain(
275
- 'plugins.entries.voice-call.config.provider must be "twilio" or "telnyx" when realtime.enabled is true',
276
- );
277
- });
278
- });
279
- });
280
-
281
- describe("resolveVoiceCallConfig session routing", () => {
282
- it("enables the pre-answer stale call reaper by default", () => {
283
- const config = resolveVoiceCallConfig({ enabled: true, provider: "mock" });
284
-
285
- expect(config.staleCallReaperSeconds).toBe(120);
286
- });
287
-
288
- it("keeps voice sessions scoped by phone by default", () => {
289
- const config = resolveVoiceCallConfig({ enabled: true, provider: "mock" });
290
-
291
- expect(config.sessionScope).toBe("per-phone");
292
- expect(
293
- resolveVoiceCallSessionKey({
294
- config,
295
- callId: "call-123",
296
- phone: "+1 (555) 000-1111",
297
- }),
298
- ).toBe("voice:15550001111");
299
- });
300
-
301
- it("can scope voice sessions to each call", () => {
302
- const config = resolveVoiceCallConfig({
303
- enabled: true,
304
- provider: "mock",
305
- sessionScope: "per-call",
306
- });
307
-
308
- expect(config.sessionScope).toBe("per-call");
309
- expect(
310
- resolveVoiceCallSessionKey({
311
- config,
312
- callId: "call-123",
313
- phone: "+1 (555) 000-1111",
314
- }),
315
- ).toBe("voice:call:call-123");
316
- });
317
-
318
- it("preserves explicit voice session keys", () => {
319
- const config = resolveVoiceCallConfig({
320
- enabled: true,
321
- provider: "mock",
322
- sessionScope: "per-call",
323
- });
324
-
325
- expect(
326
- resolveVoiceCallSessionKey({
327
- config,
328
- callId: "call-123",
329
- phone: "+1 (555) 000-1111",
330
- explicitSessionKey: "meet-room-1",
331
- }),
332
- ).toBe("meet-room-1");
333
- });
334
-
335
- it("resolves per-number inbound route overrides over global voice settings", () => {
336
- const config = resolveVoiceCallConfig({
337
- enabled: true,
338
- provider: "mock",
339
- inboundGreeting: "Hello from global.",
340
- agentId: "main",
341
- responseModel: "openai/gpt-5.4-mini",
342
- responseSystemPrompt: "Global voice assistant.",
343
- responseTimeoutMs: 10000,
344
- tts: {
345
- provider: "openai",
346
- providers: {
347
- openai: { voice: "coral", speed: 1 },
348
- },
349
- },
350
- numbers: {
351
- "+15550001111": {
352
- inboundGreeting: "Silver Fox Cards, how can I help?",
353
- agentId: "cards",
354
- responseModel: "openai/gpt-5.5",
355
- responseSystemPrompt: "You are a baseball card expert.",
356
- responseTimeoutMs: 20000,
357
- tts: {
358
- providers: {
359
- openai: { voice: "alloy" },
360
- },
361
- },
362
- },
363
- },
364
- });
365
-
366
- expect(resolveVoiceCallNumberRouteKey(config, "+1 (555) 000-1111")).toBe("+15550001111");
367
- const effective = resolveVoiceCallEffectiveConfig(config, "+1 (555) 000-1111");
368
-
369
- expect(effective.numberRouteKey).toBe("+15550001111");
370
- expect(effective.config.inboundGreeting).toBe("Silver Fox Cards, how can I help?");
371
- expect(effective.config.agentId).toBe("cards");
372
- expect(effective.config.responseModel).toBe("openai/gpt-5.5");
373
- expect(effective.config.responseSystemPrompt).toBe("You are a baseball card expert.");
374
- expect(effective.config.responseTimeoutMs).toBe(20000);
375
- expect(effective.config.tts?.provider).toBe("openai");
376
- expect(effective.config.tts?.providers?.openai).toEqual({ voice: "alloy", speed: 1 });
377
- });
378
-
379
- it("falls back to global voice settings when no per-number route matches", () => {
380
- const config = resolveVoiceCallConfig({
381
- enabled: true,
382
- provider: "mock",
383
- inboundGreeting: "Hello from global.",
384
- numbers: {
385
- "+15550001111": {
386
- inboundGreeting: "Hello from route.",
387
- },
388
- },
389
- });
390
-
391
- const effective = resolveVoiceCallEffectiveConfig(config, "+15550002222");
392
-
393
- expect(effective.numberRouteKey).toBeUndefined();
394
- expect(effective.config).toBe(config);
395
- expect(effective.config.inboundGreeting).toBe("Hello from global.");
396
- });
397
- });
398
-
399
- describe("normalizeVoiceCallConfig", () => {
400
- it("fills nested runtime defaults from a partial config boundary", () => {
401
- const normalized = normalizeVoiceCallConfig({
402
- enabled: true,
403
- provider: "mock",
404
- streaming: {
405
- enabled: true,
406
- streamPath: "/custom-stream",
407
- },
408
- });
409
-
410
- expect(normalized.serve.path).toBe("/voice/webhook");
411
- expect(normalized.streaming.streamPath).toBe("/custom-stream");
412
- expect(normalized.streaming.provider).toBeUndefined();
413
- expect(normalized.streaming.providers).toStrictEqual({});
414
- expect(normalized.realtime.streamPath).toBe("/voice/stream/realtime");
415
- expect(normalized.realtime.toolPolicy).toBe("safe-read-only");
416
- expect(normalized.realtime.consultPolicy).toBe("auto");
417
- expect(normalized.realtime.fastContext).toEqual({
418
- enabled: false,
419
- timeoutMs: 800,
420
- maxResults: 3,
421
- sources: ["memory", "sessions"],
422
- fallbackToConsult: false,
423
- });
424
- expect(normalized.realtime.consultThinkingLevel).toBeUndefined();
425
- expect(normalized.realtime.consultFastMode).toBeUndefined();
426
- expect(normalized.realtime.agentContext).toEqual({
427
- enabled: false,
428
- maxChars: 6000,
429
- includeIdentity: true,
430
- includeSystemPrompt: true,
431
- includeWorkspaceFiles: true,
432
- files: ["SOUL.md", "IDENTITY.md", "USER.md"],
433
- });
434
- expect(normalized.realtime.instructions).toContain("klaw_agent_consult");
435
- expect(normalized.tunnel.provider).toBe("none");
436
- expect(normalized.webhookSecurity.allowedHosts).toStrictEqual([]);
437
- });
438
-
439
- it("derives the realtime stream path from a custom webhook path", () => {
440
- const normalized = normalizeVoiceCallConfig({
441
- enabled: true,
442
- provider: "twilio",
443
- serve: {
444
- path: "/custom/webhook",
445
- },
446
- });
447
-
448
- expect(normalized.realtime.streamPath).toBe("/custom/stream/realtime");
449
- });
450
-
451
- it("accepts partial nested TTS overrides and preserves nested objects", () => {
452
- const normalized = normalizeVoiceCallConfig({
453
- tts: {
454
- provider: "elevenlabs",
455
- providers: {
456
- elevenlabs: {
457
- apiKey: {
458
- source: "env",
459
- provider: "elevenlabs",
460
- id: "ELEVENLABS_API_KEY",
461
- },
462
- voiceSettings: {
463
- speed: 1.1,
464
- },
465
- },
466
- },
467
- },
468
- });
469
-
470
- const { tts, elevenlabs } = requireElevenLabsTtsConfig(normalized);
471
- expect(tts.provider).toBe("elevenlabs");
472
- expect(elevenlabs.apiKey).toEqual({
473
- source: "env",
474
- provider: "elevenlabs",
475
- id: "ELEVENLABS_API_KEY",
476
- });
477
- expect(elevenlabs.voiceSettings).toEqual({ speed: 1.1 });
478
- });
479
- });
480
-
481
- describe("resolveVoiceCallConfig realtime settings", () => {
482
- it("preserves configured realtime instructions without env indirection", () => {
483
- const resolved = resolveVoiceCallConfig({
484
- enabled: true,
485
- provider: "twilio",
486
- realtime: {
487
- enabled: true,
488
- instructions: "Stay concise.",
489
- },
490
- });
491
-
492
- expect(resolved.realtime.instructions).toBe("Stay concise.");
493
- expect(resolved.realtime.toolPolicy).toBe("safe-read-only");
494
- expect(resolved.realtime.consultPolicy).toBe("auto");
495
- expect(resolved.realtime.provider).toBeUndefined();
496
- });
497
-
498
- it("preserves configured realtime consult overrides", () => {
499
- const resolved = resolveVoiceCallConfig({
500
- enabled: true,
501
- provider: "mock",
502
- realtime: {
503
- consultThinkingLevel: "low",
504
- consultFastMode: true,
505
- },
506
- });
507
-
508
- expect(resolved.realtime.consultThinkingLevel).toBe("low");
509
- expect(resolved.realtime.consultFastMode).toBe(true);
510
- });
511
-
512
- it("rejects invalid realtime consult thinking levels", () => {
513
- expect(() =>
514
- resolveVoiceCallConfig({
515
- enabled: true,
516
- provider: "mock",
517
- realtime: {
518
- consultThinkingLevel: "turbo",
519
- },
520
- } as never),
521
- ).toThrow(/Invalid option/);
522
- });
523
-
524
- it("leaves responseModel unset so voice responses can inherit runtime defaults", () => {
525
- const resolved = resolveVoiceCallConfig({
526
- enabled: true,
527
- provider: "mock",
528
- });
529
-
530
- expect(resolved.responseModel).toBeUndefined();
531
- });
532
-
533
- it("preserves the configured voice response agent id", () => {
534
- const resolved = resolveVoiceCallConfig({
535
- enabled: true,
536
- provider: "mock",
537
- agentId: "voice",
538
- });
539
-
540
- expect(resolved.agentId).toBe("voice");
541
- });
542
- });