vellum 0.2.13 → 0.2.14
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/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +113 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +137 -18
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +62 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +27 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +93 -7
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +8 -0
- package/src/calls/elevenlabs-config.ts +7 -5
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +32 -37
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +29 -7
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +10 -4
- package/src/config/schema.ts +80 -21
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for TwilioConversationRelayProvider — signature validation
|
|
3
|
-
* fail-closed auth token behavior.
|
|
2
|
+
* Tests for TwilioConversationRelayProvider — signature validation,
|
|
3
|
+
* fail-closed auth token behavior, and caller ID eligibility checks.
|
|
4
4
|
*/
|
|
5
|
-
import { describe, test, expect, beforeEach, mock } from 'bun:test';
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
|
|
6
6
|
import { createHmac } from 'node:crypto';
|
|
7
7
|
import { mkdtempSync } from 'node:fs';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
@@ -176,4 +176,154 @@ describe('TwilioConversationRelayProvider', () => {
|
|
|
176
176
|
}
|
|
177
177
|
});
|
|
178
178
|
});
|
|
179
|
+
|
|
180
|
+
describe('checkCallerIdEligibility', () => {
|
|
181
|
+
let originalFetch: typeof globalThis.fetch;
|
|
182
|
+
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
originalFetch = globalThis.fetch;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
afterEach(() => {
|
|
188
|
+
globalThis.fetch = originalFetch;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('returns eligible when number is found in IncomingPhoneNumbers', async () => {
|
|
192
|
+
const provider = new TwilioConversationRelayProvider();
|
|
193
|
+
|
|
194
|
+
globalThis.fetch = (async (url: RequestInfo | URL): Promise<Response> => {
|
|
195
|
+
const urlStr = String(url);
|
|
196
|
+
if (urlStr.includes('/IncomingPhoneNumbers.json')) {
|
|
197
|
+
return new Response(
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
incoming_phone_numbers: [{ sid: 'PN_test', phone_number: '+15550001111' }],
|
|
200
|
+
}),
|
|
201
|
+
{ status: 200 },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
// Should not reach OutgoingCallerIds since IncomingPhoneNumbers matched
|
|
205
|
+
return new Response(JSON.stringify({ outgoing_caller_ids: [] }), { status: 200 });
|
|
206
|
+
}) as typeof fetch;
|
|
207
|
+
|
|
208
|
+
const result = await provider.checkCallerIdEligibility('+15550001111');
|
|
209
|
+
expect(result.eligible).toBe(true);
|
|
210
|
+
expect(result.reason).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('returns eligible when number is found in OutgoingCallerIds', async () => {
|
|
214
|
+
const provider = new TwilioConversationRelayProvider();
|
|
215
|
+
|
|
216
|
+
globalThis.fetch = (async (url: RequestInfo | URL): Promise<Response> => {
|
|
217
|
+
const urlStr = String(url);
|
|
218
|
+
if (urlStr.includes('/IncomingPhoneNumbers.json')) {
|
|
219
|
+
return new Response(
|
|
220
|
+
JSON.stringify({ incoming_phone_numbers: [] }),
|
|
221
|
+
{ status: 200 },
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (urlStr.includes('/OutgoingCallerIds.json')) {
|
|
225
|
+
return new Response(
|
|
226
|
+
JSON.stringify({
|
|
227
|
+
outgoing_caller_ids: [{ sid: 'PNverified', phone_number: '+15550001111' }],
|
|
228
|
+
}),
|
|
229
|
+
{ status: 200 },
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return new Response('Not found', { status: 404 });
|
|
233
|
+
}) as typeof fetch;
|
|
234
|
+
|
|
235
|
+
const result = await provider.checkCallerIdEligibility('+15550001111');
|
|
236
|
+
expect(result.eligible).toBe(true);
|
|
237
|
+
expect(result.reason).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('returns ineligible when number is not found in either endpoint', async () => {
|
|
241
|
+
const provider = new TwilioConversationRelayProvider();
|
|
242
|
+
|
|
243
|
+
globalThis.fetch = (async (url: RequestInfo | URL): Promise<Response> => {
|
|
244
|
+
const urlStr = String(url);
|
|
245
|
+
if (urlStr.includes('/IncomingPhoneNumbers.json')) {
|
|
246
|
+
return new Response(
|
|
247
|
+
JSON.stringify({ incoming_phone_numbers: [] }),
|
|
248
|
+
{ status: 200 },
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (urlStr.includes('/OutgoingCallerIds.json')) {
|
|
252
|
+
return new Response(
|
|
253
|
+
JSON.stringify({ outgoing_caller_ids: [] }),
|
|
254
|
+
{ status: 200 },
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return new Response('Not found', { status: 404 });
|
|
258
|
+
}) as typeof fetch;
|
|
259
|
+
|
|
260
|
+
const result = await provider.checkCallerIdEligibility('+15559999999');
|
|
261
|
+
expect(result.eligible).toBe(false);
|
|
262
|
+
expect(result.reason).toContain('not owned by or verified');
|
|
263
|
+
expect(result.reason).toContain('Twilio Console');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('throws when both API calls return non-ok responses', async () => {
|
|
267
|
+
const provider = new TwilioConversationRelayProvider();
|
|
268
|
+
|
|
269
|
+
globalThis.fetch = (async (url: RequestInfo | URL): Promise<Response> => {
|
|
270
|
+
const urlStr = String(url);
|
|
271
|
+
if (urlStr.includes('/IncomingPhoneNumbers.json')) {
|
|
272
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
273
|
+
}
|
|
274
|
+
if (urlStr.includes('/OutgoingCallerIds.json')) {
|
|
275
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
276
|
+
}
|
|
277
|
+
return new Response('Not found', { status: 404 });
|
|
278
|
+
}) as typeof fetch;
|
|
279
|
+
|
|
280
|
+
await expect(provider.checkCallerIdEligibility('+15550001111')).rejects.toThrow(
|
|
281
|
+
'Unable to verify caller ID eligibility',
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('throws when only one API call fails but the other succeeds with no match', async () => {
|
|
286
|
+
const provider = new TwilioConversationRelayProvider();
|
|
287
|
+
|
|
288
|
+
globalThis.fetch = (async (url: RequestInfo | URL): Promise<Response> => {
|
|
289
|
+
const urlStr = String(url);
|
|
290
|
+
if (urlStr.includes('/IncomingPhoneNumbers.json')) {
|
|
291
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
292
|
+
}
|
|
293
|
+
if (urlStr.includes('/OutgoingCallerIds.json')) {
|
|
294
|
+
return new Response(
|
|
295
|
+
JSON.stringify({ outgoing_caller_ids: [] }),
|
|
296
|
+
{ status: 200 },
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return new Response('Not found', { status: 404 });
|
|
300
|
+
}) as typeof fetch;
|
|
301
|
+
|
|
302
|
+
await expect(provider.checkCallerIdEligibility('+15550001111')).rejects.toThrow(
|
|
303
|
+
'Unable to verify caller ID eligibility',
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('passes correct phone number as query parameter', async () => {
|
|
308
|
+
const provider = new TwilioConversationRelayProvider();
|
|
309
|
+
const capturedUrls: string[] = [];
|
|
310
|
+
|
|
311
|
+
globalThis.fetch = (async (url: RequestInfo | URL): Promise<Response> => {
|
|
312
|
+
capturedUrls.push(String(url));
|
|
313
|
+
const urlStr = String(url);
|
|
314
|
+
if (urlStr.includes('/IncomingPhoneNumbers.json')) {
|
|
315
|
+
return new Response(
|
|
316
|
+
JSON.stringify({ incoming_phone_numbers: [{ sid: 'PN1' }] }),
|
|
317
|
+
{ status: 200 },
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return new Response(JSON.stringify({ outgoing_caller_ids: [] }), { status: 200 });
|
|
321
|
+
}) as typeof fetch;
|
|
322
|
+
|
|
323
|
+
await provider.checkCallerIdEligibility('+15550001111');
|
|
324
|
+
|
|
325
|
+
expect(capturedUrls[0]).toContain('PhoneNumber=%2B15550001111');
|
|
326
|
+
expect(capturedUrls[0]).toContain('/IncomingPhoneNumbers.json');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
179
329
|
});
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler-level tests for ElevenLabs voice webhook branches in handleVoiceWebhook.
|
|
3
|
+
*
|
|
4
|
+
* Tests the WS-A (invalid profile + fallback semantics) and WS-B
|
|
5
|
+
* (elevenlabs_agent guard) paths by mocking resolveVoiceQualityProfile
|
|
6
|
+
* to return specific profiles and asserting on HTTP response status/body.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test';
|
|
9
|
+
import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
const testDir = realpathSync(mkdtempSync(join(tmpdir(), 'twilio-routes-11labs-test-')));
|
|
14
|
+
|
|
15
|
+
mock.module('../util/platform.js', () => ({
|
|
16
|
+
getRootDir: () => testDir,
|
|
17
|
+
getDataDir: () => testDir,
|
|
18
|
+
isMacOS: () => process.platform === 'darwin',
|
|
19
|
+
isLinux: () => process.platform === 'linux',
|
|
20
|
+
isWindows: () => process.platform === 'win32',
|
|
21
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
22
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
23
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
24
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
25
|
+
ensureDataDir: () => {},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock.module('../util/logger.js', () => ({
|
|
29
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
30
|
+
get: () => () => {},
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
mock.module('../config/loader.js', () => ({
|
|
35
|
+
getConfig: () => ({
|
|
36
|
+
model: 'test',
|
|
37
|
+
provider: 'test',
|
|
38
|
+
apiKeys: {},
|
|
39
|
+
memory: { enabled: false },
|
|
40
|
+
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
41
|
+
secretDetection: { enabled: false },
|
|
42
|
+
}),
|
|
43
|
+
loadConfig: () => ({
|
|
44
|
+
model: 'test',
|
|
45
|
+
provider: 'test',
|
|
46
|
+
apiKeys: {},
|
|
47
|
+
memory: { enabled: false },
|
|
48
|
+
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
49
|
+
secretDetection: { enabled: false },
|
|
50
|
+
calls: {
|
|
51
|
+
voice: {
|
|
52
|
+
mode: 'twilio_standard',
|
|
53
|
+
language: 'en-US',
|
|
54
|
+
transcriptionProvider: 'Deepgram',
|
|
55
|
+
fallbackToStandardOnError: true,
|
|
56
|
+
elevenlabs: {
|
|
57
|
+
voiceId: '',
|
|
58
|
+
voiceModelId: '',
|
|
59
|
+
speed: 1.0,
|
|
60
|
+
stability: 0.5,
|
|
61
|
+
similarityBoost: 0.75,
|
|
62
|
+
useSpeakerBoost: true,
|
|
63
|
+
agentId: '',
|
|
64
|
+
apiBaseUrl: 'https://api.elevenlabs.io',
|
|
65
|
+
registerCallTimeoutMs: 5000,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
ingress: { enabled: false, publicBaseUrl: '' },
|
|
70
|
+
}),
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
mock.module('../security/secure-keys.js', () => ({
|
|
74
|
+
getSecureKey: () => undefined,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
mock.module('../calls/twilio-provider.js', () => ({
|
|
78
|
+
TwilioConversationRelayProvider: class {
|
|
79
|
+
readonly name = 'twilio';
|
|
80
|
+
static getAuthToken(): string | null { return null; }
|
|
81
|
+
static verifyWebhookSignature(): boolean { return true; }
|
|
82
|
+
async initiateCall() { return { callSid: 'CA_mock_test' }; }
|
|
83
|
+
async endCall() { return; }
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
mock.module('../calls/twilio-config.js', () => ({
|
|
88
|
+
getTwilioConfig: () => ({
|
|
89
|
+
accountSid: 'AC_test',
|
|
90
|
+
authToken: 'test-auth-token',
|
|
91
|
+
phoneNumber: '+15550001111',
|
|
92
|
+
webhookBaseUrl: 'https://test.example.com',
|
|
93
|
+
wssBaseUrl: 'wss://test.example.com',
|
|
94
|
+
}),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// Mock ElevenLabs client — should never be called when guard is active.
|
|
98
|
+
// If any test path reaches register-call, the mock will throw to fail the test.
|
|
99
|
+
const mockRegisterCall = mock(() => { throw new Error('register-call should not be reached while guard is active'); });
|
|
100
|
+
|
|
101
|
+
mock.module('../calls/elevenlabs-client.js', () => ({
|
|
102
|
+
ElevenLabsClient: class {
|
|
103
|
+
registerCall = mockRegisterCall;
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
mock.module('../calls/elevenlabs-config.js', () => ({
|
|
108
|
+
getElevenLabsConfig: () => ({
|
|
109
|
+
apiBaseUrl: 'https://api.elevenlabs.io',
|
|
110
|
+
apiKey: 'test-key',
|
|
111
|
+
agentId: 'agent-abc',
|
|
112
|
+
registerCallTimeoutMs: 5000,
|
|
113
|
+
}),
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
// Mock resolveVoiceQualityProfile and isVoiceProfileValid so we can control
|
|
117
|
+
// the profile returned to handleVoiceWebhook per-test.
|
|
118
|
+
import type { VoiceQualityProfile } from '../calls/voice-quality.js';
|
|
119
|
+
|
|
120
|
+
let mockProfile: VoiceQualityProfile = {
|
|
121
|
+
mode: 'twilio_standard',
|
|
122
|
+
language: 'en-US',
|
|
123
|
+
transcriptionProvider: 'Deepgram',
|
|
124
|
+
ttsProvider: 'Google',
|
|
125
|
+
voice: 'Google.en-US-Journey-O',
|
|
126
|
+
fallbackToStandardOnError: true,
|
|
127
|
+
validationErrors: [],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const mockResolveVoiceQualityProfile = mock(() => mockProfile);
|
|
131
|
+
|
|
132
|
+
mock.module('../calls/voice-quality.js', () => ({
|
|
133
|
+
resolveVoiceQualityProfile: mockResolveVoiceQualityProfile,
|
|
134
|
+
isVoiceProfileValid: (profile: VoiceQualityProfile) => profile.validationErrors.length === 0,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Mock the ingress URL to avoid config lookup issues
|
|
138
|
+
mock.module('../inbound/public-ingress-urls.js', () => ({
|
|
139
|
+
getTwilioRelayUrl: () => 'wss://test.example.com/v1/calls/relay',
|
|
140
|
+
getPublicBaseUrl: () => 'https://test.example.com',
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
144
|
+
import { conversations } from '../memory/schema.js';
|
|
145
|
+
import {
|
|
146
|
+
createCallSession,
|
|
147
|
+
updateCallSession,
|
|
148
|
+
} from '../calls/call-store.js';
|
|
149
|
+
import { handleVoiceWebhook } from '../calls/twilio-routes.js';
|
|
150
|
+
|
|
151
|
+
initializeDb();
|
|
152
|
+
|
|
153
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
let ensuredConvIds = new Set<string>();
|
|
156
|
+
|
|
157
|
+
function ensureConversation(id: string): void {
|
|
158
|
+
if (ensuredConvIds.has(id)) return;
|
|
159
|
+
const db = getDb();
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
db.insert(conversations).values({
|
|
162
|
+
id,
|
|
163
|
+
title: `Test conversation ${id}`,
|
|
164
|
+
createdAt: now,
|
|
165
|
+
updatedAt: now,
|
|
166
|
+
}).run();
|
|
167
|
+
ensuredConvIds.add(id);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resetTables() {
|
|
171
|
+
const db = getDb();
|
|
172
|
+
db.run('DELETE FROM processed_callbacks');
|
|
173
|
+
db.run('DELETE FROM call_pending_questions');
|
|
174
|
+
db.run('DELETE FROM call_events');
|
|
175
|
+
db.run('DELETE FROM call_sessions');
|
|
176
|
+
db.run('DELETE FROM conversations');
|
|
177
|
+
ensuredConvIds = new Set();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function createTestSession(convId: string, callSid: string) {
|
|
181
|
+
ensureConversation(convId);
|
|
182
|
+
const session = createCallSession({
|
|
183
|
+
conversationId: convId,
|
|
184
|
+
provider: 'twilio',
|
|
185
|
+
fromNumber: '+15550001111',
|
|
186
|
+
toNumber: '+15559998888',
|
|
187
|
+
task: 'test task',
|
|
188
|
+
});
|
|
189
|
+
updateCallSession(session.id, { providerCallSid: callSid });
|
|
190
|
+
return session;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function makeVoiceRequest(sessionId: string, params: Record<string, string>): Request {
|
|
194
|
+
return new Request(`http://127.0.0.1/v1/calls/twilio/voice-webhook?callSessionId=${sessionId}`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
197
|
+
body: new URLSearchParams(params).toString(),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Tests ──────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
describe('handleVoiceWebhook ElevenLabs branches', () => {
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
resetTables();
|
|
206
|
+
// Reset mock to default implementation
|
|
207
|
+
mockResolveVoiceQualityProfile.mockReset();
|
|
208
|
+
mockRegisterCall.mockClear();
|
|
209
|
+
// Reset to standard default profile between tests
|
|
210
|
+
mockProfile = {
|
|
211
|
+
mode: 'twilio_standard',
|
|
212
|
+
language: 'en-US',
|
|
213
|
+
transcriptionProvider: 'Deepgram',
|
|
214
|
+
ttsProvider: 'Google',
|
|
215
|
+
voice: 'Google.en-US-Journey-O',
|
|
216
|
+
fallbackToStandardOnError: true,
|
|
217
|
+
validationErrors: [],
|
|
218
|
+
};
|
|
219
|
+
mockResolveVoiceQualityProfile.mockImplementation(() => mockProfile);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
afterAll(() => {
|
|
223
|
+
resetDb();
|
|
224
|
+
try { rmSync(testDir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── WS-A: Invalid config + fallback disabled => 500 ────────────────
|
|
228
|
+
test('twilio_elevenlabs_tts invalid config with fallback disabled returns 500', async () => {
|
|
229
|
+
mockProfile = {
|
|
230
|
+
mode: 'twilio_elevenlabs_tts',
|
|
231
|
+
language: 'en-US',
|
|
232
|
+
transcriptionProvider: 'Deepgram',
|
|
233
|
+
ttsProvider: 'ElevenLabs',
|
|
234
|
+
voice: '',
|
|
235
|
+
fallbackToStandardOnError: false,
|
|
236
|
+
validationErrors: ['voiceId is required'],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const session = createTestSession('conv-11labs-1', 'CA_11labs_1');
|
|
240
|
+
const req = makeVoiceRequest(session.id, { CallSid: 'CA_11labs_1' });
|
|
241
|
+
|
|
242
|
+
const res = await handleVoiceWebhook(req);
|
|
243
|
+
|
|
244
|
+
expect(res.status).toBe(500);
|
|
245
|
+
const body = await res.text();
|
|
246
|
+
expect(body).toContain('Voice quality configuration error');
|
|
247
|
+
expect(body).toContain('voiceId is required');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── WS-A: Invalid config + fallback enabled => standard TwiML ──────
|
|
251
|
+
test('twilio_elevenlabs_tts invalid config with fallback enabled returns standard TwiML', async () => {
|
|
252
|
+
// When fallback is enabled and voiceId is missing, resolveVoiceQualityProfile
|
|
253
|
+
// falls back to standard mode but retains validation errors.
|
|
254
|
+
mockProfile = {
|
|
255
|
+
mode: 'twilio_standard',
|
|
256
|
+
language: 'en-US',
|
|
257
|
+
transcriptionProvider: 'Deepgram',
|
|
258
|
+
ttsProvider: 'Google',
|
|
259
|
+
voice: 'Google.en-US-Journey-O',
|
|
260
|
+
fallbackToStandardOnError: true,
|
|
261
|
+
validationErrors: ['calls.voice.elevenlabs.voiceId is empty; falling back to twilio_standard'],
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const session = createTestSession('conv-11labs-2', 'CA_11labs_2');
|
|
265
|
+
const req = makeVoiceRequest(session.id, { CallSid: 'CA_11labs_2' });
|
|
266
|
+
|
|
267
|
+
const res = await handleVoiceWebhook(req);
|
|
268
|
+
|
|
269
|
+
expect(res.status).toBe(200);
|
|
270
|
+
const twiml = await res.text();
|
|
271
|
+
expect(twiml).toContain('ttsProvider="Google"');
|
|
272
|
+
expect(twiml).toContain('<ConversationRelay');
|
|
273
|
+
expect(twiml).toContain('voice="Google.en-US-Journey-O"');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── WS-B: elevenlabs_agent strict mode (fallback false) => 501 ─────
|
|
277
|
+
test('elevenlabs_agent with fallback disabled returns 501', async () => {
|
|
278
|
+
mockProfile = {
|
|
279
|
+
mode: 'elevenlabs_agent',
|
|
280
|
+
language: 'en-US',
|
|
281
|
+
transcriptionProvider: 'Deepgram',
|
|
282
|
+
ttsProvider: 'ElevenLabs',
|
|
283
|
+
voice: 'voice123-turbo_v2_5-1_0.5_0.75',
|
|
284
|
+
agentId: 'agent-abc',
|
|
285
|
+
fallbackToStandardOnError: false,
|
|
286
|
+
validationErrors: [],
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const session = createTestSession('conv-11labs-3', 'CA_11labs_3');
|
|
290
|
+
const req = makeVoiceRequest(session.id, { CallSid: 'CA_11labs_3' });
|
|
291
|
+
|
|
292
|
+
const res = await handleVoiceWebhook(req);
|
|
293
|
+
|
|
294
|
+
expect(res.status).toBe(501);
|
|
295
|
+
const body = await res.text();
|
|
296
|
+
expect(body).toContain('consultation bridging');
|
|
297
|
+
expect(body).toContain('elevenlabs_agent mode is restricted');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ── WS-B: elevenlabs_agent with fallback true => standard TwiML ────
|
|
301
|
+
test('elevenlabs_agent with fallback enabled returns standard TwiML', async () => {
|
|
302
|
+
// First call returns the elevenlabs_agent profile (triggers the guard)
|
|
303
|
+
mockResolveVoiceQualityProfile.mockImplementationOnce(() => ({
|
|
304
|
+
mode: 'elevenlabs_agent' as const,
|
|
305
|
+
language: 'en-US',
|
|
306
|
+
transcriptionProvider: 'Deepgram',
|
|
307
|
+
ttsProvider: 'ElevenLabs',
|
|
308
|
+
voice: 'voice123-turbo_v2_5-1_0.5_0.75',
|
|
309
|
+
agentId: 'agent-abc',
|
|
310
|
+
fallbackToStandardOnError: true,
|
|
311
|
+
validationErrors: [],
|
|
312
|
+
}));
|
|
313
|
+
// Second call returns the standard profile (used for TwiML generation)
|
|
314
|
+
mockResolveVoiceQualityProfile.mockImplementationOnce(() => ({
|
|
315
|
+
mode: 'twilio_standard' as const,
|
|
316
|
+
language: 'en-US',
|
|
317
|
+
transcriptionProvider: 'Deepgram',
|
|
318
|
+
ttsProvider: 'Google',
|
|
319
|
+
voice: 'Google.en-US-Journey-O',
|
|
320
|
+
fallbackToStandardOnError: true,
|
|
321
|
+
validationErrors: [],
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
const session = createTestSession('conv-11labs-4', 'CA_11labs_4');
|
|
325
|
+
const req = makeVoiceRequest(session.id, { CallSid: 'CA_11labs_4' });
|
|
326
|
+
|
|
327
|
+
const res = await handleVoiceWebhook(req);
|
|
328
|
+
|
|
329
|
+
expect(res.status).toBe(200);
|
|
330
|
+
const twiml = await res.text();
|
|
331
|
+
// When elevenlabs_agent is guarded with fallback enabled, the handler
|
|
332
|
+
// replaces the profile with a standard twilio_standard profile and
|
|
333
|
+
// generates TwiML with Google TTS instead of ElevenLabs.
|
|
334
|
+
expect(twiml).toContain('<ConversationRelay');
|
|
335
|
+
expect(twiml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
|
|
336
|
+
expect(twiml).toContain('<Response>');
|
|
337
|
+
expect(twiml).toContain('<Connect>');
|
|
338
|
+
expect(twiml).toContain('ttsProvider="Google"');
|
|
339
|
+
expect(twiml).toContain('voice="Google.en-US-Journey-O"');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ── Guard prevents register-call attempt ────────────────────────────
|
|
343
|
+
test('guarded elevenlabs_agent does not attempt ElevenLabs register-call', async () => {
|
|
344
|
+
// Configure as elevenlabs_agent with fallback (guard will fire)
|
|
345
|
+
mockResolveVoiceQualityProfile.mockImplementationOnce(() => ({
|
|
346
|
+
mode: 'elevenlabs_agent' as const,
|
|
347
|
+
language: 'en-US',
|
|
348
|
+
transcriptionProvider: 'Deepgram',
|
|
349
|
+
ttsProvider: 'ElevenLabs',
|
|
350
|
+
voice: 'voice123-turbo_v2_5-1_0.5_0.75',
|
|
351
|
+
agentId: 'agent-abc',
|
|
352
|
+
fallbackToStandardOnError: true,
|
|
353
|
+
validationErrors: [],
|
|
354
|
+
}));
|
|
355
|
+
mockResolveVoiceQualityProfile.mockImplementationOnce(() => ({
|
|
356
|
+
mode: 'twilio_standard' as const,
|
|
357
|
+
language: 'en-US',
|
|
358
|
+
transcriptionProvider: 'Deepgram',
|
|
359
|
+
ttsProvider: 'Google',
|
|
360
|
+
voice: 'Google.en-US-Journey-O',
|
|
361
|
+
fallbackToStandardOnError: true,
|
|
362
|
+
validationErrors: [],
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
const session = createTestSession('conv-11labs-5', 'CA_11labs_5');
|
|
366
|
+
const req = makeVoiceRequest(session.id, { CallSid: 'CA_11labs_5' });
|
|
367
|
+
|
|
368
|
+
const res = await handleVoiceWebhook(req);
|
|
369
|
+
|
|
370
|
+
expect(res.status).toBe(200);
|
|
371
|
+
// The ElevenLabs register-call mock was never invoked — the guard
|
|
372
|
+
// blocked the entire mode before any ElevenLabs API interaction.
|
|
373
|
+
expect(mockRegisterCall).not.toHaveBeenCalled();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -39,11 +39,11 @@ describe('generateTwiML with voice quality profile', () => {
|
|
|
39
39
|
language: 'en-US',
|
|
40
40
|
transcriptionProvider: 'Deepgram',
|
|
41
41
|
ttsProvider: 'ElevenLabs',
|
|
42
|
-
voice: 'voice123-turbo_v2_5-
|
|
42
|
+
voice: 'voice123-turbo_v2_5-1_0.5_0.75',
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
expect(twiml).toContain('ttsProvider="ElevenLabs"');
|
|
46
|
-
expect(twiml).toContain('voice="voice123-turbo_v2_5-
|
|
46
|
+
expect(twiml).toContain('voice="voice123-turbo_v2_5-1_0.5_0.75"');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
test('voice attribute reflects configured voice for twilio_standard mode', () => {
|
|
@@ -62,10 +62,10 @@ describe('generateTwiML with voice quality profile', () => {
|
|
|
62
62
|
language: 'en-US',
|
|
63
63
|
transcriptionProvider: 'Deepgram',
|
|
64
64
|
ttsProvider: 'ElevenLabs',
|
|
65
|
-
voice: 'abc123-turbo_v2_5-
|
|
65
|
+
voice: 'abc123-turbo_v2_5-1_0.5_0.75',
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
expect(twiml).toContain('voice="abc123-turbo_v2_5-
|
|
68
|
+
expect(twiml).toContain('voice="abc123-turbo_v2_5-1_0.5_0.75"');
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
test('language attribute reflects configured language', () => {
|