@vellumai/assistant 0.6.3 → 0.6.4

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 (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -2,8 +2,13 @@
2
2
  * Unit tests for TwiML generation with voice quality profiles.
3
3
  *
4
4
  * Tests that generateTwiML correctly uses profile values for
5
- * ttsProvider, voice, language, transcriptionProvider,
6
- * and interruptSensitivity.
5
+ * ttsProvider, voice, and language, and that STT attributes
6
+ * (transcriptionProvider, speechModel, interruptSensitivity, hints)
7
+ * are correctly rendered in the ConversationRelay TwiML output.
8
+ *
9
+ * Speech config objects are constructed inline — STT provider routing
10
+ * is driven by `services.stt` via `resolveTelephonySttRouting` in the
11
+ * production path. These tests exercise the TwiML serializer only.
7
12
  */
8
13
  import { describe, expect, mock, test } from "bun:test";
9
14
 
@@ -14,7 +19,114 @@ mock.module("../util/logger.js", () => ({
14
19
  }),
15
20
  }));
16
21
 
17
- import { generateTwiML } from "../calls/twilio-routes.js";
22
+ import type { TwilioRelaySpeechConfig } from "../calls/twilio-routes.js";
23
+ import { generateStreamTwiML, generateTwiML } from "../calls/twilio-routes.js";
24
+
25
+ // ── Helper to build speech config inline ─────────────────────────────
26
+
27
+ function speechConfig(
28
+ transcriptionProvider: string,
29
+ speechModel: string | undefined,
30
+ interruptSensitivity: string,
31
+ hints?: string,
32
+ ): TwilioRelaySpeechConfig {
33
+ return {
34
+ transcriptionProvider,
35
+ speechModel,
36
+ hints: hints && hints.length > 0 ? hints : undefined,
37
+ interruptSensitivity,
38
+ };
39
+ }
40
+
41
+ // ── TwilioRelaySpeechConfig rendering ────────────────────────────────
42
+
43
+ describe("TwilioRelaySpeechConfig rendering in TwiML", () => {
44
+ test("Deepgram provider and speechModel are rendered", () => {
45
+ const twiml = generateTwiML(
46
+ "sc-1",
47
+ "wss://test.example.com/relay",
48
+ null,
49
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
50
+ speechConfig("Deepgram", "nova-3", "low"),
51
+ );
52
+ expect(twiml).toContain('transcriptionProvider="Deepgram"');
53
+ expect(twiml).toContain('speechModel="nova-3"');
54
+ expect(twiml).toContain('interruptSensitivity="low"');
55
+ expect(twiml).not.toContain("hints=");
56
+ });
57
+
58
+ test("explicit speechModel is preserved in TwiML", () => {
59
+ const twiml = generateTwiML(
60
+ "sc-2",
61
+ "wss://test.example.com/relay",
62
+ null,
63
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
64
+ speechConfig("Deepgram", "nova-2-phonecall", "medium"),
65
+ );
66
+ expect(twiml).toContain('transcriptionProvider="Deepgram"');
67
+ expect(twiml).toContain('speechModel="nova-2-phonecall"');
68
+ expect(twiml).toContain('interruptSensitivity="medium"');
69
+ });
70
+
71
+ test("Google provider with no speechModel omits speechModel attribute", () => {
72
+ const twiml = generateTwiML(
73
+ "sc-3",
74
+ "wss://test.example.com/relay",
75
+ null,
76
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
77
+ speechConfig("Google", undefined, "low"),
78
+ );
79
+ expect(twiml).toContain('transcriptionProvider="Google"');
80
+ expect(twiml).not.toContain("speechModel=");
81
+ });
82
+
83
+ test("Google provider with explicit model includes speechModel", () => {
84
+ const twiml = generateTwiML(
85
+ "sc-4",
86
+ "wss://test.example.com/relay",
87
+ null,
88
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
89
+ speechConfig("Google", "telephony", "low"),
90
+ );
91
+ expect(twiml).toContain('transcriptionProvider="Google"');
92
+ expect(twiml).toContain('speechModel="telephony"');
93
+ });
94
+
95
+ test("hints included when non-empty", () => {
96
+ const twiml = generateTwiML(
97
+ "sc-5",
98
+ "wss://test.example.com/relay",
99
+ null,
100
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
101
+ speechConfig("Deepgram", "nova-3", "low", "Alice,Bob"),
102
+ );
103
+ expect(twiml).toContain('hints="Alice,Bob"');
104
+ });
105
+
106
+ test("hints omitted when empty string", () => {
107
+ const twiml = generateTwiML(
108
+ "sc-6",
109
+ "wss://test.example.com/relay",
110
+ null,
111
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
112
+ speechConfig("Deepgram", "nova-3", "low", ""),
113
+ );
114
+ expect(twiml).not.toContain("hints=");
115
+ });
116
+
117
+ test("hints omitted when not provided", () => {
118
+ const twiml = generateTwiML(
119
+ "sc-7",
120
+ "wss://test.example.com/relay",
121
+ null,
122
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
123
+ speechConfig("Deepgram", "nova-3", "low"),
124
+ );
125
+ expect(twiml).not.toContain("hints=");
126
+ });
127
+ });
128
+
129
+ // ── generateTwiML with speech config ─────────────────────────────────
18
130
 
19
131
  describe("generateTwiML with voice quality profile", () => {
20
132
  const callSessionId = "test-session-123";
@@ -22,13 +134,17 @@ describe("generateTwiML with voice quality profile", () => {
22
134
  const welcomeGreeting = "Hello, how can I help?";
23
135
 
24
136
  test('TwiML includes ttsProvider="Google" when profile specifies Google', () => {
25
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
26
- language: "en-US",
27
- transcriptionProvider: "Deepgram",
28
- ttsProvider: "Google",
29
- voice: "Google.en-US-Journey-O",
30
- interruptSensitivity: "low",
31
- });
137
+ const twiml = generateTwiML(
138
+ callSessionId,
139
+ relayUrl,
140
+ welcomeGreeting,
141
+ {
142
+ language: "en-US",
143
+ ttsProvider: "Google",
144
+ voice: "Google.en-US-Journey-O",
145
+ },
146
+ speechConfig("Deepgram", "nova-3", "low"),
147
+ );
32
148
 
33
149
  expect(twiml).toContain('ttsProvider="Google"');
34
150
  expect(twiml).toContain('voice="Google.en-US-Journey-O"');
@@ -37,124 +153,216 @@ describe("generateTwiML with voice quality profile", () => {
37
153
  });
38
154
 
39
155
  test('TwiML includes ttsProvider="ElevenLabs" when profile specifies ElevenLabs', () => {
40
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
41
- language: "en-US",
42
- transcriptionProvider: "Deepgram",
43
- ttsProvider: "ElevenLabs",
44
- voice: "voice123-turbo_v2_5-1_0.5_0.75",
45
- interruptSensitivity: "low",
46
- });
156
+ const twiml = generateTwiML(
157
+ callSessionId,
158
+ relayUrl,
159
+ welcomeGreeting,
160
+ {
161
+ language: "en-US",
162
+ ttsProvider: "ElevenLabs",
163
+ voice: "voice123-turbo_v2_5-1_0.5_0.75",
164
+ },
165
+ speechConfig("Deepgram", "nova-3", "low"),
166
+ );
47
167
 
48
168
  expect(twiml).toContain('ttsProvider="ElevenLabs"');
49
169
  expect(twiml).toContain('voice="voice123-turbo_v2_5-1_0.5_0.75"');
50
170
  });
51
171
 
52
172
  test("voice attribute reflects configured Google voice", () => {
53
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
54
- language: "en-US",
55
- transcriptionProvider: "Deepgram",
56
- ttsProvider: "Google",
57
- voice: "Google.en-US-Journey-O",
58
- interruptSensitivity: "low",
59
- });
173
+ const twiml = generateTwiML(
174
+ callSessionId,
175
+ relayUrl,
176
+ welcomeGreeting,
177
+ {
178
+ language: "en-US",
179
+ ttsProvider: "Google",
180
+ voice: "Google.en-US-Journey-O",
181
+ },
182
+ speechConfig("Deepgram", "nova-3", "low"),
183
+ );
60
184
 
61
185
  expect(twiml).toContain('voice="Google.en-US-Journey-O"');
62
186
  });
63
187
 
64
188
  test("voice attribute reflects configured ElevenLabs voice", () => {
65
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
66
- language: "en-US",
67
- transcriptionProvider: "Deepgram",
68
- ttsProvider: "ElevenLabs",
69
- voice: "abc123-turbo_v2_5-1_0.5_0.75",
70
- interruptSensitivity: "low",
71
- });
189
+ const twiml = generateTwiML(
190
+ callSessionId,
191
+ relayUrl,
192
+ welcomeGreeting,
193
+ {
194
+ language: "en-US",
195
+ ttsProvider: "ElevenLabs",
196
+ voice: "abc123-turbo_v2_5-1_0.5_0.75",
197
+ },
198
+ speechConfig("Deepgram", "nova-3", "low"),
199
+ );
72
200
 
73
201
  expect(twiml).toContain('voice="abc123-turbo_v2_5-1_0.5_0.75"');
74
202
  });
75
203
 
76
204
  test("language attribute reflects configured language", () => {
77
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
78
- language: "es-MX",
79
- transcriptionProvider: "Google",
80
- ttsProvider: "Google",
81
- voice: "Google.es-MX-Standard-A",
82
- interruptSensitivity: "low",
83
- });
205
+ const twiml = generateTwiML(
206
+ callSessionId,
207
+ relayUrl,
208
+ welcomeGreeting,
209
+ {
210
+ language: "es-MX",
211
+ ttsProvider: "Google",
212
+ voice: "Google.es-MX-Standard-A",
213
+ },
214
+ speechConfig("Google", undefined, "low"),
215
+ );
84
216
 
85
217
  expect(twiml).toContain('language="es-MX"');
86
218
  });
87
219
 
88
- test("transcriptionProvider reflects configured value", () => {
89
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
90
- language: "en-US",
91
- transcriptionProvider: "Google",
92
- ttsProvider: "Google",
93
- voice: "Google.en-US-Journey-O",
94
- interruptSensitivity: "low",
95
- });
220
+ test("transcriptionProvider reflects Deepgram via speech config", () => {
221
+ const twiml = generateTwiML(
222
+ callSessionId,
223
+ relayUrl,
224
+ welcomeGreeting,
225
+ {
226
+ language: "en-US",
227
+ ttsProvider: "ElevenLabs",
228
+ voice: "voice123",
229
+ },
230
+ speechConfig("Deepgram", "nova-3", "low"),
231
+ );
232
+
233
+ expect(twiml).toContain('transcriptionProvider="Deepgram"');
234
+ expect(twiml).toContain('speechModel="nova-3"');
235
+ });
236
+
237
+ test("transcriptionProvider reflects Google via speech config", () => {
238
+ const twiml = generateTwiML(
239
+ callSessionId,
240
+ relayUrl,
241
+ welcomeGreeting,
242
+ {
243
+ language: "en-US",
244
+ ttsProvider: "ElevenLabs",
245
+ voice: "voice123",
246
+ },
247
+ speechConfig("Google", undefined, "low"),
248
+ );
96
249
 
97
250
  expect(twiml).toContain('transcriptionProvider="Google"');
251
+ expect(twiml).not.toContain("speechModel=");
252
+ });
253
+
254
+ test("Google with explicit telephony model includes speechModel", () => {
255
+ const twiml = generateTwiML(
256
+ callSessionId,
257
+ relayUrl,
258
+ welcomeGreeting,
259
+ {
260
+ language: "en-US",
261
+ ttsProvider: "ElevenLabs",
262
+ voice: "voice123",
263
+ },
264
+ speechConfig("Google", "telephony", "low"),
265
+ );
266
+
267
+ expect(twiml).toContain('transcriptionProvider="Google"');
268
+ expect(twiml).toContain('speechModel="telephony"');
269
+ });
270
+
271
+ test("Deepgram with explicit model preserves it", () => {
272
+ const twiml = generateTwiML(
273
+ callSessionId,
274
+ relayUrl,
275
+ welcomeGreeting,
276
+ {
277
+ language: "en-US",
278
+ ttsProvider: "ElevenLabs",
279
+ voice: "voice123",
280
+ },
281
+ speechConfig("Deepgram", "nova-2-phonecall", "low"),
282
+ );
283
+
284
+ expect(twiml).toContain('transcriptionProvider="Deepgram"');
285
+ expect(twiml).toContain('speechModel="nova-2-phonecall"');
98
286
  });
99
287
 
100
288
  test("TwiML properly escapes XML characters in profile values", () => {
101
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
102
- language: "en-US",
103
- transcriptionProvider: "Deepgram",
104
- ttsProvider: "Google",
105
- voice: 'voice<>&"test',
106
- interruptSensitivity: "low",
107
- });
289
+ const twiml = generateTwiML(
290
+ callSessionId,
291
+ relayUrl,
292
+ welcomeGreeting,
293
+ {
294
+ language: "en-US",
295
+ ttsProvider: "Google",
296
+ voice: 'voice<>&"test',
297
+ },
298
+ speechConfig("Deepgram", "nova-3", "low"),
299
+ );
108
300
 
109
301
  expect(twiml).toContain('voice="voice&lt;&gt;&amp;&quot;test"');
110
302
  expect(twiml).not.toContain('voice="voice<>&"test"');
111
303
  });
112
304
 
113
305
  test("TwiML includes callSessionId in relay URL", () => {
114
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
115
- language: "en-US",
116
- transcriptionProvider: "Deepgram",
117
- ttsProvider: "Google",
118
- voice: "Google.en-US-Journey-O",
119
- interruptSensitivity: "low",
120
- });
306
+ const twiml = generateTwiML(
307
+ callSessionId,
308
+ relayUrl,
309
+ welcomeGreeting,
310
+ {
311
+ language: "en-US",
312
+ ttsProvider: "Google",
313
+ voice: "Google.en-US-Journey-O",
314
+ },
315
+ speechConfig("Deepgram", "nova-3", "low"),
316
+ );
121
317
 
122
318
  expect(twiml).toContain(`callSessionId=${callSessionId}`);
123
319
  });
124
320
 
125
321
  test("TwiML includes interruptible and dtmfDetection attributes", () => {
126
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
127
- language: "en-US",
128
- transcriptionProvider: "Deepgram",
129
- ttsProvider: "Google",
130
- voice: "Google.en-US-Journey-O",
131
- interruptSensitivity: "low",
132
- });
322
+ const twiml = generateTwiML(
323
+ callSessionId,
324
+ relayUrl,
325
+ welcomeGreeting,
326
+ {
327
+ language: "en-US",
328
+ ttsProvider: "Google",
329
+ voice: "Google.en-US-Journey-O",
330
+ },
331
+ speechConfig("Deepgram", "nova-3", "low"),
332
+ );
133
333
 
134
334
  expect(twiml).toContain('interruptible="true"');
135
335
  expect(twiml).toContain('dtmfDetection="true"');
136
336
  });
137
337
 
138
338
  test("TwiML omits welcomeGreeting attribute when not provided", () => {
139
- const twiml = generateTwiML(callSessionId, relayUrl, null, {
140
- language: "en-US",
141
- transcriptionProvider: "Deepgram",
142
- ttsProvider: "Google",
143
- voice: "Google.en-US-Journey-O",
144
- interruptSensitivity: "low",
145
- });
339
+ const twiml = generateTwiML(
340
+ callSessionId,
341
+ relayUrl,
342
+ null,
343
+ {
344
+ language: "en-US",
345
+ ttsProvider: "Google",
346
+ voice: "Google.en-US-Journey-O",
347
+ },
348
+ speechConfig("Deepgram", "nova-3", "low"),
349
+ );
146
350
 
147
351
  expect(twiml).not.toContain("welcomeGreeting=");
148
352
  });
149
353
 
150
- test('TwiML includes interruptSensitivity="low" when profile has low', () => {
151
- const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
152
- language: "en-US",
153
- transcriptionProvider: "Deepgram",
154
- ttsProvider: "Google",
155
- voice: "Google.en-US-Journey-O",
156
- interruptSensitivity: "low",
157
- });
354
+ test('TwiML includes interruptSensitivity="low" from speech config', () => {
355
+ const twiml = generateTwiML(
356
+ callSessionId,
357
+ relayUrl,
358
+ welcomeGreeting,
359
+ {
360
+ language: "en-US",
361
+ ttsProvider: "Google",
362
+ voice: "Google.en-US-Journey-O",
363
+ },
364
+ speechConfig("Deepgram", "nova-3", "low"),
365
+ );
158
366
 
159
367
  expect(twiml).toContain('interruptSensitivity="low"');
160
368
  });
@@ -166,81 +374,72 @@ describe("generateTwiML with voice quality profile", () => {
166
374
  welcomeGreeting,
167
375
  {
168
376
  language: "en-US",
169
- transcriptionProvider: "Deepgram",
170
377
  ttsProvider: "Google",
171
378
  voice: "Google.en-US-Journey-O",
172
- interruptSensitivity: "medium",
173
379
  },
380
+ speechConfig("Deepgram", "nova-3", "medium"),
174
381
  );
175
382
 
176
383
  expect(twimlMedium).toContain('interruptSensitivity="medium"');
177
384
 
178
- const twimlHigh = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
179
- language: "en-US",
180
- transcriptionProvider: "Deepgram",
181
- ttsProvider: "Google",
182
- voice: "Google.en-US-Journey-O",
183
- interruptSensitivity: "high",
184
- });
385
+ const twimlHigh = generateTwiML(
386
+ callSessionId,
387
+ relayUrl,
388
+ welcomeGreeting,
389
+ {
390
+ language: "en-US",
391
+ ttsProvider: "Google",
392
+ voice: "Google.en-US-Journey-O",
393
+ },
394
+ speechConfig("Deepgram", "nova-3", "high"),
395
+ );
185
396
 
186
397
  expect(twimlHigh).toContain('interruptSensitivity="high"');
187
398
  });
188
399
 
189
- test("hints attribute present when hints string is non-empty", () => {
400
+ test("hints attribute present when speech config includes hints", () => {
190
401
  const twiml = generateTwiML(
191
402
  callSessionId,
192
403
  relayUrl,
193
404
  welcomeGreeting,
194
405
  {
195
406
  language: "en-US",
196
- transcriptionProvider: "Deepgram",
197
407
  ttsProvider: "ElevenLabs",
198
408
  voice: "voice123",
199
- interruptSensitivity: "low",
200
409
  },
201
- undefined,
202
- undefined,
203
- "Alice,Bob,Vellum",
410
+ speechConfig("Deepgram", "nova-3", "low", "Alice,Bob,Vellum"),
204
411
  );
205
412
 
206
413
  expect(twiml).toContain('hints="Alice,Bob,Vellum"');
207
414
  });
208
415
 
209
- test("hints attribute omitted when hints string is empty", () => {
416
+ test("hints attribute omitted when speech config has no hints", () => {
210
417
  const twiml = generateTwiML(
211
418
  callSessionId,
212
419
  relayUrl,
213
420
  welcomeGreeting,
214
421
  {
215
422
  language: "en-US",
216
- transcriptionProvider: "Deepgram",
217
423
  ttsProvider: "ElevenLabs",
218
424
  voice: "voice123",
219
- interruptSensitivity: "low",
220
425
  },
221
- undefined,
222
- undefined,
223
- "",
426
+ speechConfig("Deepgram", "nova-3", "low"),
224
427
  );
225
428
 
226
429
  expect(twiml).not.toContain("hints=");
227
430
  });
228
431
 
229
- test("hints attribute omitted when hints parameter is undefined", () => {
432
+ test("hints attribute omitted when speech config hints is empty", () => {
230
433
  const twiml = generateTwiML(
231
434
  callSessionId,
232
435
  relayUrl,
233
436
  welcomeGreeting,
234
437
  {
235
438
  language: "en-US",
236
- transcriptionProvider: "Deepgram",
237
439
  ttsProvider: "ElevenLabs",
238
440
  voice: "voice123",
239
- interruptSensitivity: "low",
240
441
  },
241
- undefined,
242
- undefined,
243
- undefined,
442
+ speechConfig("Deepgram", "nova-3", "low", ""),
244
443
  );
245
444
 
246
445
  expect(twiml).not.toContain("hints=");
@@ -253,14 +452,15 @@ describe("generateTwiML with voice quality profile", () => {
253
452
  welcomeGreeting,
254
453
  {
255
454
  language: "en-US",
256
- transcriptionProvider: "Deepgram",
257
455
  ttsProvider: "ElevenLabs",
258
456
  voice: "voice123",
259
- interruptSensitivity: "low",
260
457
  },
261
- undefined,
262
- undefined,
263
- 'O\'Brien,Smith & Jones,"Dr. Lee"',
458
+ speechConfig(
459
+ "Deepgram",
460
+ "nova-3",
461
+ "low",
462
+ 'O\'Brien,Smith & Jones,"Dr. Lee"',
463
+ ),
264
464
  );
265
465
 
266
466
  expect(twiml).toContain(
@@ -269,3 +469,201 @@ describe("generateTwiML with voice quality profile", () => {
269
469
  expect(twiml).not.toContain("hints=\"O'Brien");
270
470
  });
271
471
  });
472
+
473
+ // ── generateStreamTwiML unit tests ────────────────────────────────────
474
+ // Tests for the <Connect><Stream> TwiML generator used by the
475
+ // media-stream-custom strategy (e.g. OpenAI Whisper).
476
+
477
+ describe("generateStreamTwiML", () => {
478
+ const callSessionId = "stream-session-1";
479
+ const streamUrl = "wss://test.example.com/webhooks/twilio/media-stream";
480
+
481
+ test("emits <Stream> element with callSessionId as path segment", () => {
482
+ const twiml = generateStreamTwiML(callSessionId, streamUrl);
483
+
484
+ expect(twiml).toContain("<Stream");
485
+ // callSessionId is encoded as a path segment, not a query param
486
+ expect(twiml).toContain(
487
+ `url="wss://test.example.com/webhooks/twilio/media-stream/${callSessionId}"`,
488
+ );
489
+ expect(twiml).not.toContain("<ConversationRelay");
490
+ // No query params should be present
491
+ expect(twiml).not.toContain("?callSessionId=");
492
+ });
493
+
494
+ test("includes callSessionId as <Parameter>", () => {
495
+ const twiml = generateStreamTwiML(callSessionId, streamUrl);
496
+
497
+ expect(twiml).toContain(
498
+ `<Parameter name="callSessionId" value="${callSessionId}" />`,
499
+ );
500
+ });
501
+
502
+ test("includes auth token as path segment and as <Parameter> when provided", () => {
503
+ const twiml = generateStreamTwiML(
504
+ callSessionId,
505
+ streamUrl,
506
+ "test-relay-token-123",
507
+ );
508
+
509
+ // Token as path segment for gateway auth during WS upgrade
510
+ expect(twiml).toContain(
511
+ `url="wss://test.example.com/webhooks/twilio/media-stream/${callSessionId}/test-relay-token-123"`,
512
+ );
513
+ // Token also in <Parameter> for Twilio start event payload
514
+ expect(twiml).toContain(
515
+ '<Parameter name="token" value="test-relay-token-123" />',
516
+ );
517
+ });
518
+
519
+ test("omits token from URL path and Parameter when not provided", () => {
520
+ const twiml = generateStreamTwiML(callSessionId, streamUrl);
521
+
522
+ expect(twiml).not.toContain('name="token"');
523
+ // URL should only have callSessionId as path segment, no token
524
+ expect(twiml).toContain(
525
+ `url="wss://test.example.com/webhooks/twilio/media-stream/${callSessionId}"`,
526
+ );
527
+ });
528
+
529
+ test("includes custom parameters as <Parameter> elements", () => {
530
+ const twiml = generateStreamTwiML(callSessionId, streamUrl, "tok", {
531
+ verificationSessionId: "vs-123",
532
+ });
533
+
534
+ expect(twiml).toContain(
535
+ '<Parameter name="verificationSessionId" value="vs-123" />',
536
+ );
537
+ expect(twiml).toContain(
538
+ `<Parameter name="callSessionId" value="${callSessionId}" />`,
539
+ );
540
+ expect(twiml).toContain('<Parameter name="token" value="tok" />');
541
+ });
542
+
543
+ test("callSessionId cannot be overridden by customParameters", () => {
544
+ const twiml = generateStreamTwiML(callSessionId, streamUrl, undefined, {
545
+ callSessionId: "attacker-session",
546
+ });
547
+
548
+ // The real callSessionId must win over the custom parameter
549
+ expect(twiml).toContain(
550
+ `<Parameter name="callSessionId" value="${callSessionId}" />`,
551
+ );
552
+ expect(twiml).not.toContain('value="attacker-session"');
553
+ // URL path must also have the correct callSessionId
554
+ expect(twiml).toContain(`/media-stream/${callSessionId}`);
555
+ expect(twiml).not.toContain("attacker-session");
556
+ });
557
+
558
+ test("does not include ConversationRelay STT attributes", () => {
559
+ const twiml = generateStreamTwiML(callSessionId, streamUrl);
560
+
561
+ expect(twiml).not.toContain("transcriptionProvider=");
562
+ expect(twiml).not.toContain("speechModel=");
563
+ expect(twiml).not.toContain("interruptSensitivity=");
564
+ expect(twiml).not.toContain("ttsProvider=");
565
+ expect(twiml).not.toContain("voice=");
566
+ expect(twiml).not.toContain("language=");
567
+ });
568
+
569
+ test("wraps in valid TwiML structure", () => {
570
+ const twiml = generateStreamTwiML(callSessionId, streamUrl);
571
+
572
+ expect(twiml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
573
+ expect(twiml).toContain("<Response>");
574
+ expect(twiml).toContain("<Connect>");
575
+ expect(twiml).toContain("</Stream>");
576
+ expect(twiml).toContain("</Connect>");
577
+ expect(twiml).toContain("</Response>");
578
+ });
579
+
580
+ test("URL-encodes special characters in callSessionId path segment", () => {
581
+ const specialId = "sess&id=1/2";
582
+ const twiml = generateStreamTwiML(specialId, streamUrl);
583
+
584
+ // Special characters must be percent-encoded in the path segment
585
+ expect(twiml).toContain("/media-stream/sess%26id%3D1%2F2");
586
+ // But the <Parameter> value should have the raw value (XML-escaped)
587
+ expect(twiml).toContain(
588
+ '<Parameter name="callSessionId" value="sess&amp;id=1/2" />',
589
+ );
590
+ });
591
+ });
592
+
593
+ // ── Provider-conditional TwiML generation ─────────────────────────────
594
+ // These tests verify that the two TwiML generators produce the correct
595
+ // structure for each provider strategy:
596
+ // - Deepgram/Google -> ConversationRelay with STT attributes
597
+ // - OpenAI Whisper -> Stream with no STT attributes
598
+
599
+ describe("Provider-conditional TwiML generation", () => {
600
+ const callSessionId = "provider-test-1";
601
+ const relayUrl = "wss://test.example.com/v1/calls/relay";
602
+ const streamUrl = "wss://test.example.com/webhooks/twilio/media-stream";
603
+
604
+ test("Deepgram: ConversationRelay with transcriptionProvider=Deepgram and speechModel=nova-3", () => {
605
+ const twiml = generateTwiML(
606
+ callSessionId,
607
+ relayUrl,
608
+ null,
609
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
610
+ speechConfig("Deepgram", "nova-3", "low"),
611
+ );
612
+
613
+ expect(twiml).toContain("<ConversationRelay");
614
+ expect(twiml).not.toContain("<Stream");
615
+ expect(twiml).toContain('transcriptionProvider="Deepgram"');
616
+ expect(twiml).toContain('speechModel="nova-3"');
617
+ });
618
+
619
+ test("Google Gemini: ConversationRelay with transcriptionProvider=Google (no speechModel when unset)", () => {
620
+ const twiml = generateTwiML(
621
+ callSessionId,
622
+ relayUrl,
623
+ null,
624
+ { language: "en-US", ttsProvider: "ElevenLabs", voice: "voice1" },
625
+ speechConfig("Google", undefined, "low"),
626
+ );
627
+
628
+ expect(twiml).toContain("<ConversationRelay");
629
+ expect(twiml).not.toContain("<Stream");
630
+ expect(twiml).toContain('transcriptionProvider="Google"');
631
+ expect(twiml).not.toContain("speechModel=");
632
+ });
633
+
634
+ test("OpenAI Whisper: Stream path with callSessionId/token in path segments", () => {
635
+ const twiml = generateStreamTwiML(callSessionId, streamUrl, "tok");
636
+
637
+ expect(twiml).toContain("<Stream");
638
+ expect(twiml).not.toContain("<ConversationRelay");
639
+ expect(twiml).not.toContain("transcriptionProvider=");
640
+ expect(twiml).not.toContain("speechModel=");
641
+ expect(twiml).toContain(
642
+ `<Parameter name="callSessionId" value="${callSessionId}" />`,
643
+ );
644
+ // Metadata is path-based, not query-based
645
+ expect(twiml).toContain(`/media-stream/${callSessionId}/tok`);
646
+ expect(twiml).not.toContain("?callSessionId=");
647
+ });
648
+
649
+ test("ConversationRelay element contains all required STT-related attributes", () => {
650
+ const twiml = generateTwiML(
651
+ callSessionId,
652
+ relayUrl,
653
+ null,
654
+ {
655
+ language: "en-US",
656
+ ttsProvider: "Google",
657
+ voice: "Google.en-US-Journey-O",
658
+ },
659
+ speechConfig("Deepgram", "nova-3", "medium", "Alice,Bob"),
660
+ );
661
+
662
+ expect(twiml).toContain('transcriptionProvider="Deepgram"');
663
+ expect(twiml).toContain('speechModel="nova-3"');
664
+ expect(twiml).toContain('interruptSensitivity="medium"');
665
+ expect(twiml).toContain('hints="Alice,Bob"');
666
+ expect(twiml).toContain('interruptible="true"');
667
+ expect(twiml).toContain('dtmfDetection="true"');
668
+ });
669
+ });