@openclaw/voice-call 2026.5.2 → 2026.5.3-beta.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.
- package/dist/api.js +2 -0
- package/dist/call-status-CXldV5o8.js +32 -0
- package/dist/cli-metadata.js +12 -0
- package/dist/config-7w04YpHh.js +548 -0
- package/dist/config-compat-B0me39_4.js +129 -0
- package/dist/guarded-json-api-Btx5EE4w.js +591 -0
- package/dist/http-headers-BrnxBasF.js +10 -0
- package/dist/index.js +1284 -0
- package/dist/mock-CeKvfVEd.js +135 -0
- package/dist/plivo-B-a7KFoT.js +393 -0
- package/dist/realtime-handler-B63CIDP2.js +325 -0
- package/dist/realtime-transcription.runtime-B2h70y2W.js +2 -0
- package/dist/realtime-voice.runtime-Bkh4nvLn.js +2 -0
- package/dist/response-generator-BrcmwDZU.js +182 -0
- package/dist/response-model-CyF5K80p.js +12 -0
- package/dist/runtime-api.js +6 -0
- package/dist/runtime-entry-88ytYAQa.js +3119 -0
- package/dist/runtime-entry.js +2 -0
- package/dist/setup-api.js +37 -0
- package/dist/telnyx-jjBE8boz.js +260 -0
- package/dist/twilio-1OqbcXLL.js +676 -0
- package/dist/voice-mapping-BYDGdWGx.js +40 -0
- package/package.json +14 -6
- package/api.ts +0 -16
- package/cli-metadata.ts +0 -10
- package/config-api.ts +0 -12
- package/index.test.ts +0 -943
- package/index.ts +0 -794
- package/runtime-api.ts +0 -20
- package/runtime-entry.ts +0 -1
- package/setup-api.ts +0 -47
- package/src/allowlist.test.ts +0 -18
- package/src/allowlist.ts +0 -19
- package/src/cli.ts +0 -845
- package/src/config-compat.test.ts +0 -120
- package/src/config-compat.ts +0 -227
- package/src/config.test.ts +0 -479
- package/src/config.ts +0 -808
- package/src/core-bridge.ts +0 -14
- package/src/deep-merge.test.ts +0 -40
- package/src/deep-merge.ts +0 -23
- package/src/gateway-continue-operation.ts +0 -200
- package/src/http-headers.test.ts +0 -16
- package/src/http-headers.ts +0 -15
- package/src/manager/context.ts +0 -42
- package/src/manager/events.test.ts +0 -581
- package/src/manager/events.ts +0 -288
- package/src/manager/lifecycle.ts +0 -53
- package/src/manager/lookup.test.ts +0 -52
- package/src/manager/lookup.ts +0 -35
- package/src/manager/outbound.test.ts +0 -528
- package/src/manager/outbound.ts +0 -486
- package/src/manager/state.ts +0 -48
- package/src/manager/store.ts +0 -106
- package/src/manager/timers.test.ts +0 -129
- package/src/manager/timers.ts +0 -113
- package/src/manager/twiml.test.ts +0 -13
- package/src/manager/twiml.ts +0 -17
- package/src/manager.closed-loop.test.ts +0 -236
- package/src/manager.inbound-allowlist.test.ts +0 -188
- package/src/manager.notify.test.ts +0 -377
- package/src/manager.restore.test.ts +0 -183
- package/src/manager.test-harness.ts +0 -127
- package/src/manager.ts +0 -392
- package/src/media-stream.test.ts +0 -768
- package/src/media-stream.ts +0 -708
- package/src/providers/base.ts +0 -97
- package/src/providers/mock.test.ts +0 -78
- package/src/providers/mock.ts +0 -185
- package/src/providers/plivo.test.ts +0 -93
- package/src/providers/plivo.ts +0 -601
- package/src/providers/shared/call-status.test.ts +0 -24
- package/src/providers/shared/call-status.ts +0 -24
- package/src/providers/shared/guarded-json-api.test.ts +0 -106
- package/src/providers/shared/guarded-json-api.ts +0 -42
- package/src/providers/telnyx.test.ts +0 -340
- package/src/providers/telnyx.ts +0 -394
- package/src/providers/twilio/api.test.ts +0 -145
- package/src/providers/twilio/api.ts +0 -93
- package/src/providers/twilio/twiml-policy.test.ts +0 -84
- package/src/providers/twilio/twiml-policy.ts +0 -87
- package/src/providers/twilio/webhook.ts +0 -34
- package/src/providers/twilio.test.ts +0 -591
- package/src/providers/twilio.ts +0 -861
- package/src/providers/twilio.types.ts +0 -17
- package/src/realtime-defaults.ts +0 -3
- package/src/realtime-fast-context.test.ts +0 -88
- package/src/realtime-fast-context.ts +0 -165
- package/src/realtime-transcription.runtime.ts +0 -4
- package/src/realtime-voice.runtime.ts +0 -5
- package/src/response-generator.test.ts +0 -321
- package/src/response-generator.ts +0 -318
- package/src/response-model.test.ts +0 -71
- package/src/response-model.ts +0 -23
- package/src/runtime.test.ts +0 -536
- package/src/runtime.ts +0 -510
- package/src/telephony-audio.test.ts +0 -61
- package/src/telephony-audio.ts +0 -12
- package/src/telephony-tts.test.ts +0 -196
- package/src/telephony-tts.ts +0 -235
- package/src/test-fixtures.ts +0 -73
- package/src/tts-provider-voice.test.ts +0 -34
- package/src/tts-provider-voice.ts +0 -21
- package/src/tunnel.test.ts +0 -166
- package/src/tunnel.ts +0 -314
- package/src/types.ts +0 -291
- package/src/utils.test.ts +0 -17
- package/src/utils.ts +0 -14
- package/src/voice-mapping.test.ts +0 -34
- package/src/voice-mapping.ts +0 -68
- package/src/webhook/realtime-handler.test.ts +0 -598
- package/src/webhook/realtime-handler.ts +0 -485
- package/src/webhook/stale-call-reaper.test.ts +0 -88
- package/src/webhook/stale-call-reaper.ts +0 -38
- package/src/webhook/tailscale.test.ts +0 -214
- package/src/webhook/tailscale.ts +0 -129
- package/src/webhook-exposure.test.ts +0 -33
- package/src/webhook-exposure.ts +0 -84
- package/src/webhook-security.test.ts +0 -770
- package/src/webhook-security.ts +0 -994
- package/src/webhook.hangup-once.lifecycle.test.ts +0 -135
- package/src/webhook.test.ts +0 -1470
- package/src/webhook.ts +0 -908
- package/src/webhook.types.ts +0 -5
- package/src/websocket-test-support.ts +0 -72
- package/tsconfig.json +0 -16
package/src/config.test.ts
DELETED
|
@@ -1,479 +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)).toMatchObject({ 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)).toMatchObject({ 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)).toMatchObject({ 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).toEqual([]);
|
|
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)).toMatchObject({ 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)).toMatchObject({ valid: true, errors: [] });
|
|
188
|
-
|
|
189
|
-
const skippedVerification = createBaseConfig("telnyx");
|
|
190
|
-
skippedVerification.skipSignatureVerification = true;
|
|
191
|
-
skippedVerification.telnyx = { apiKey: "KEY123", connectionId: "CONN456" };
|
|
192
|
-
expect(validateProviderConfig(skippedVerification)).toMatchObject({
|
|
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).toEqual([]);
|
|
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
|
-
});
|
|
255
|
-
|
|
256
|
-
describe("resolveVoiceCallConfig", () => {
|
|
257
|
-
it("enables the pre-answer stale call reaper by default", () => {
|
|
258
|
-
const config = resolveVoiceCallConfig({ enabled: true, provider: "mock" });
|
|
259
|
-
|
|
260
|
-
expect(config.staleCallReaperSeconds).toBe(120);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("keeps voice sessions scoped by phone by default", () => {
|
|
264
|
-
const config = resolveVoiceCallConfig({ enabled: true, provider: "mock" });
|
|
265
|
-
|
|
266
|
-
expect(config.sessionScope).toBe("per-phone");
|
|
267
|
-
expect(
|
|
268
|
-
resolveVoiceCallSessionKey({
|
|
269
|
-
config,
|
|
270
|
-
callId: "call-123",
|
|
271
|
-
phone: "+1 (555) 000-1111",
|
|
272
|
-
}),
|
|
273
|
-
).toBe("voice:15550001111");
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("can scope voice sessions to each call", () => {
|
|
277
|
-
const config = resolveVoiceCallConfig({
|
|
278
|
-
enabled: true,
|
|
279
|
-
provider: "mock",
|
|
280
|
-
sessionScope: "per-call",
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
expect(config.sessionScope).toBe("per-call");
|
|
284
|
-
expect(
|
|
285
|
-
resolveVoiceCallSessionKey({
|
|
286
|
-
config,
|
|
287
|
-
callId: "call-123",
|
|
288
|
-
phone: "+1 (555) 000-1111",
|
|
289
|
-
}),
|
|
290
|
-
).toBe("voice:call:call-123");
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("preserves explicit voice session keys", () => {
|
|
294
|
-
const config = resolveVoiceCallConfig({
|
|
295
|
-
enabled: true,
|
|
296
|
-
provider: "mock",
|
|
297
|
-
sessionScope: "per-call",
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
expect(
|
|
301
|
-
resolveVoiceCallSessionKey({
|
|
302
|
-
config,
|
|
303
|
-
callId: "call-123",
|
|
304
|
-
phone: "+1 (555) 000-1111",
|
|
305
|
-
explicitSessionKey: "meet-room-1",
|
|
306
|
-
}),
|
|
307
|
-
).toBe("meet-room-1");
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("resolves per-number inbound route overrides over global voice settings", () => {
|
|
311
|
-
const config = resolveVoiceCallConfig({
|
|
312
|
-
enabled: true,
|
|
313
|
-
provider: "mock",
|
|
314
|
-
inboundGreeting: "Hello from global.",
|
|
315
|
-
agentId: "main",
|
|
316
|
-
responseModel: "openai/gpt-5.4-mini",
|
|
317
|
-
responseSystemPrompt: "Global voice assistant.",
|
|
318
|
-
responseTimeoutMs: 10000,
|
|
319
|
-
tts: {
|
|
320
|
-
provider: "openai",
|
|
321
|
-
providers: {
|
|
322
|
-
openai: { voice: "coral", speed: 1 },
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
numbers: {
|
|
326
|
-
"+15550001111": {
|
|
327
|
-
inboundGreeting: "Silver Fox Cards, how can I help?",
|
|
328
|
-
agentId: "cards",
|
|
329
|
-
responseModel: "openai/gpt-5.5",
|
|
330
|
-
responseSystemPrompt: "You are a baseball card expert.",
|
|
331
|
-
responseTimeoutMs: 20000,
|
|
332
|
-
tts: {
|
|
333
|
-
providers: {
|
|
334
|
-
openai: { voice: "alloy" },
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
expect(resolveVoiceCallNumberRouteKey(config, "+1 (555) 000-1111")).toBe("+15550001111");
|
|
342
|
-
const effective = resolveVoiceCallEffectiveConfig(config, "+1 (555) 000-1111");
|
|
343
|
-
|
|
344
|
-
expect(effective.numberRouteKey).toBe("+15550001111");
|
|
345
|
-
expect(effective.config.inboundGreeting).toBe("Silver Fox Cards, how can I help?");
|
|
346
|
-
expect(effective.config.agentId).toBe("cards");
|
|
347
|
-
expect(effective.config.responseModel).toBe("openai/gpt-5.5");
|
|
348
|
-
expect(effective.config.responseSystemPrompt).toBe("You are a baseball card expert.");
|
|
349
|
-
expect(effective.config.responseTimeoutMs).toBe(20000);
|
|
350
|
-
expect(effective.config.tts?.provider).toBe("openai");
|
|
351
|
-
expect(effective.config.tts?.providers?.openai).toEqual({ voice: "alloy", speed: 1 });
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("falls back to global voice settings when no per-number route matches", () => {
|
|
355
|
-
const config = resolveVoiceCallConfig({
|
|
356
|
-
enabled: true,
|
|
357
|
-
provider: "mock",
|
|
358
|
-
inboundGreeting: "Hello from global.",
|
|
359
|
-
numbers: {
|
|
360
|
-
"+15550001111": {
|
|
361
|
-
inboundGreeting: "Hello from route.",
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
const effective = resolveVoiceCallEffectiveConfig(config, "+15550002222");
|
|
367
|
-
|
|
368
|
-
expect(effective.numberRouteKey).toBeUndefined();
|
|
369
|
-
expect(effective.config).toBe(config);
|
|
370
|
-
expect(effective.config.inboundGreeting).toBe("Hello from global.");
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
describe("normalizeVoiceCallConfig", () => {
|
|
375
|
-
it("fills nested runtime defaults from a partial config boundary", () => {
|
|
376
|
-
const normalized = normalizeVoiceCallConfig({
|
|
377
|
-
enabled: true,
|
|
378
|
-
provider: "mock",
|
|
379
|
-
streaming: {
|
|
380
|
-
enabled: true,
|
|
381
|
-
streamPath: "/custom-stream",
|
|
382
|
-
},
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
expect(normalized.serve.path).toBe("/voice/webhook");
|
|
386
|
-
expect(normalized.streaming.streamPath).toBe("/custom-stream");
|
|
387
|
-
expect(normalized.streaming.provider).toBeUndefined();
|
|
388
|
-
expect(normalized.streaming.providers).toEqual({});
|
|
389
|
-
expect(normalized.realtime.streamPath).toBe("/voice/stream/realtime");
|
|
390
|
-
expect(normalized.realtime.toolPolicy).toBe("safe-read-only");
|
|
391
|
-
expect(normalized.realtime.fastContext).toEqual({
|
|
392
|
-
enabled: false,
|
|
393
|
-
timeoutMs: 800,
|
|
394
|
-
maxResults: 3,
|
|
395
|
-
sources: ["memory", "sessions"],
|
|
396
|
-
fallbackToConsult: false,
|
|
397
|
-
});
|
|
398
|
-
expect(normalized.realtime.instructions).toContain("openclaw_agent_consult");
|
|
399
|
-
expect(normalized.tunnel.provider).toBe("none");
|
|
400
|
-
expect(normalized.webhookSecurity.allowedHosts).toEqual([]);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it("derives the realtime stream path from a custom webhook path", () => {
|
|
404
|
-
const normalized = normalizeVoiceCallConfig({
|
|
405
|
-
enabled: true,
|
|
406
|
-
provider: "twilio",
|
|
407
|
-
serve: {
|
|
408
|
-
path: "/custom/webhook",
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
expect(normalized.realtime.streamPath).toBe("/custom/stream/realtime");
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it("accepts partial nested TTS overrides and preserves nested objects", () => {
|
|
416
|
-
const normalized = normalizeVoiceCallConfig({
|
|
417
|
-
tts: {
|
|
418
|
-
provider: "elevenlabs",
|
|
419
|
-
providers: {
|
|
420
|
-
elevenlabs: {
|
|
421
|
-
apiKey: {
|
|
422
|
-
source: "env",
|
|
423
|
-
provider: "elevenlabs",
|
|
424
|
-
id: "ELEVENLABS_API_KEY",
|
|
425
|
-
},
|
|
426
|
-
voiceSettings: {
|
|
427
|
-
speed: 1.1,
|
|
428
|
-
},
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
},
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
const { tts, elevenlabs } = requireElevenLabsTtsConfig(normalized);
|
|
435
|
-
expect(tts.provider).toBe("elevenlabs");
|
|
436
|
-
expect(elevenlabs.apiKey).toEqual({
|
|
437
|
-
source: "env",
|
|
438
|
-
provider: "elevenlabs",
|
|
439
|
-
id: "ELEVENLABS_API_KEY",
|
|
440
|
-
});
|
|
441
|
-
expect(elevenlabs.voiceSettings).toEqual({ speed: 1.1 });
|
|
442
|
-
});
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
describe("resolveVoiceCallConfig", () => {
|
|
446
|
-
it("preserves configured realtime instructions without env indirection", () => {
|
|
447
|
-
const resolved = resolveVoiceCallConfig({
|
|
448
|
-
enabled: true,
|
|
449
|
-
provider: "twilio",
|
|
450
|
-
realtime: {
|
|
451
|
-
enabled: true,
|
|
452
|
-
instructions: "Stay concise.",
|
|
453
|
-
},
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
expect(resolved.realtime.instructions).toBe("Stay concise.");
|
|
457
|
-
expect(resolved.realtime.toolPolicy).toBe("safe-read-only");
|
|
458
|
-
expect(resolved.realtime.provider).toBeUndefined();
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it("leaves responseModel unset so voice responses can inherit runtime defaults", () => {
|
|
462
|
-
const resolved = resolveVoiceCallConfig({
|
|
463
|
-
enabled: true,
|
|
464
|
-
provider: "mock",
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
expect(resolved.responseModel).toBeUndefined();
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
it("preserves the configured voice response agent id", () => {
|
|
471
|
-
const resolved = resolveVoiceCallConfig({
|
|
472
|
-
enabled: true,
|
|
473
|
-
provider: "mock",
|
|
474
|
-
agentId: "voice",
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
expect(resolved.agentId).toBe("voice");
|
|
478
|
-
});
|
|
479
|
-
});
|