@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
@@ -0,0 +1,828 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mocks — must be declared before any subject imports
5
+ // ---------------------------------------------------------------------------
6
+
7
+ // -- Logger mock ----------------------------------------------------------
8
+
9
+ /**
10
+ * Captured log messages from the resolver. Tests can assert that
11
+ * `resolveStreamingTranscriber` logs a warning when `diarize: "required"`
12
+ * is passed with a non-capable provider.
13
+ */
14
+ const loggerWarnings: Array<{ data: unknown; message: string }> = [];
15
+
16
+ mock.module("../../../util/logger.js", () => ({
17
+ getLogger: () => ({
18
+ warn: (data: unknown, message: string) => {
19
+ loggerWarnings.push({ data, message });
20
+ },
21
+ info: () => {},
22
+ debug: () => {},
23
+ error: () => {},
24
+ trace: () => {},
25
+ fatal: () => {},
26
+ child: () => ({
27
+ warn: (data: unknown, message: string) => {
28
+ loggerWarnings.push({ data, message });
29
+ },
30
+ info: () => {},
31
+ debug: () => {},
32
+ error: () => {},
33
+ trace: () => {},
34
+ fatal: () => {},
35
+ }),
36
+ }),
37
+ }));
38
+
39
+ // -- Config mock ----------------------------------------------------------
40
+
41
+ let mockConfig: Record<string, unknown> = {};
42
+
43
+ mock.module("../../../config/loader.js", () => ({
44
+ getConfig: () => mockConfig,
45
+ loadConfig: () => mockConfig,
46
+ }));
47
+
48
+ // -- Credential mock ------------------------------------------------------
49
+
50
+ let mockProviderKeys: Record<string, string | undefined> = {};
51
+
52
+ mock.module("../../../security/secure-keys.js", () => ({
53
+ getProviderKeyAsync: async (provider: string) =>
54
+ mockProviderKeys[provider] ?? undefined,
55
+ getSecureKeyAsync: async () => null,
56
+ getSecureKey: () => null,
57
+ }));
58
+
59
+ mock.module("../../../security/credential-key.js", () => ({
60
+ credentialKey: (...args: string[]) => args.join("/"),
61
+ }));
62
+
63
+ // -- Streaming adapter mocks ----------------------------------------------
64
+
65
+ /**
66
+ * Captured constructor calls for each streaming adapter. Tests assert on
67
+ * these arrays to verify the resolver plumbs options (sampleRate, diarize)
68
+ * correctly to each provider.
69
+ */
70
+ const deepgramCtorCalls: Array<{ apiKey: string; options: unknown }> = [];
71
+ const geminiCtorCalls: Array<{ apiKey: string; options: unknown }> = [];
72
+ const whisperCtorCalls: Array<{ apiKey: string; options: unknown }> = [];
73
+
74
+ mock.module("../deepgram-realtime.js", () => ({
75
+ DeepgramRealtimeTranscriber: class {
76
+ readonly providerId = "deepgram" as const;
77
+ readonly boundaryId = "daemon-streaming" as const;
78
+ constructor(apiKey: string, options: unknown) {
79
+ deepgramCtorCalls.push({ apiKey, options });
80
+ }
81
+ },
82
+ }));
83
+
84
+ mock.module("../google-gemini-live-stream.js", () => ({
85
+ GoogleGeminiLiveStreamingTranscriber: class {
86
+ readonly providerId = "google-gemini" as const;
87
+ readonly boundaryId = "daemon-streaming" as const;
88
+ constructor(apiKey: string, options: unknown) {
89
+ geminiCtorCalls.push({ apiKey, options });
90
+ }
91
+ },
92
+ }));
93
+
94
+ mock.module("../openai-whisper-stream.js", () => ({
95
+ OpenAIWhisperStreamingTranscriber: class {
96
+ readonly providerId = "openai-whisper" as const;
97
+ readonly boundaryId = "daemon-streaming" as const;
98
+ constructor(apiKey: string, options: unknown) {
99
+ whisperCtorCalls.push({ apiKey, options });
100
+ }
101
+ },
102
+ }));
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Subject import (after mocks)
106
+ //
107
+ // Use a dynamic `await import(...)` so the module-top `const log = getLogger(...)`
108
+ // in `resolve.ts` is captured by the mocked logger above. Static ESM imports
109
+ // are hoisted above all module-top statements, which would cause `resolve.ts`
110
+ // to evaluate — and call the real `getLogger` — before `mock.module(...)` runs.
111
+ // ---------------------------------------------------------------------------
112
+
113
+ const {
114
+ resolveBatchTranscriber,
115
+ resolveConversationStreamingSttCapability,
116
+ resolveStreamingTranscriber,
117
+ resolveTelephonySttCapability,
118
+ } = await import("../resolve.js");
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Helpers
122
+ // ---------------------------------------------------------------------------
123
+
124
+ function buildConfig(overrides: {
125
+ provider?: string;
126
+ }): Record<string, unknown> {
127
+ return {
128
+ services: {
129
+ stt: {
130
+ mode: "your-own",
131
+ provider: overrides.provider ?? "openai-whisper",
132
+ providers: {
133
+ "openai-whisper": {},
134
+ deepgram: {},
135
+ },
136
+ },
137
+ },
138
+ };
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Tests — resolveBatchTranscriber
143
+ // ---------------------------------------------------------------------------
144
+
145
+ describe("resolveBatchTranscriber", () => {
146
+ beforeEach(() => {
147
+ mockConfig = buildConfig({});
148
+ mockProviderKeys = {};
149
+ });
150
+
151
+ test("returns a BatchTranscriber when openai-whisper is configured and credentials are available", async () => {
152
+ mockProviderKeys["openai"] = "sk-test-key";
153
+ mockConfig = buildConfig({ provider: "openai-whisper" });
154
+
155
+ const transcriber = await resolveBatchTranscriber();
156
+
157
+ expect(transcriber).not.toBeNull();
158
+ expect(transcriber!.providerId).toBe("openai-whisper");
159
+ expect(transcriber!.boundaryId).toBe("daemon-batch");
160
+ });
161
+
162
+ test("returns null when credentials are missing for the configured provider", async () => {
163
+ mockProviderKeys = {}; // no keys at all
164
+ mockConfig = buildConfig({ provider: "openai-whisper" });
165
+
166
+ const transcriber = await resolveBatchTranscriber();
167
+
168
+ expect(transcriber).toBeNull();
169
+ });
170
+
171
+ test("returns null when configured provider is unsupported for daemon-batch", async () => {
172
+ // Force an unknown provider past the type system to simulate a future
173
+ // provider that hasn't been wired into the daemon-batch boundary yet.
174
+ mockProviderKeys["some-provider"] = "key";
175
+ mockConfig = buildConfig({ provider: "unknown-provider" as string });
176
+
177
+ const transcriber = await resolveBatchTranscriber();
178
+
179
+ expect(transcriber).toBeNull();
180
+ });
181
+
182
+ test("uses config-driven provider selection, not hardcoded OpenAI", async () => {
183
+ // Verify the resolver reads from config rather than always using "openai".
184
+ // If the config says openai-whisper, we expect credential lookup for "openai".
185
+ mockProviderKeys["openai"] = "sk-config-driven";
186
+ mockConfig = buildConfig({ provider: "openai-whisper" });
187
+
188
+ const transcriber = await resolveBatchTranscriber();
189
+
190
+ expect(transcriber).not.toBeNull();
191
+ expect(transcriber!.providerId).toBe("openai-whisper");
192
+ });
193
+
194
+ test("resolved transcriber has stable provider identity", async () => {
195
+ mockProviderKeys["openai"] = "sk-identity-test";
196
+ mockConfig = buildConfig({ provider: "openai-whisper" });
197
+
198
+ const transcriber = await resolveBatchTranscriber();
199
+
200
+ // The providerId must remain "openai-whisper" for downstream identity checks.
201
+ expect(transcriber!.providerId).toBe("openai-whisper");
202
+ expect(transcriber!.boundaryId).toBe("daemon-batch");
203
+ });
204
+
205
+ // -------------------------------------------------------------------------
206
+ // Deepgram provider resolution
207
+ // -------------------------------------------------------------------------
208
+
209
+ test("returns a BatchTranscriber when deepgram is configured and credentials are available", async () => {
210
+ mockProviderKeys["deepgram"] = "dg-test-key";
211
+ mockConfig = buildConfig({ provider: "deepgram" });
212
+
213
+ const transcriber = await resolveBatchTranscriber();
214
+
215
+ expect(transcriber).not.toBeNull();
216
+ expect(transcriber!.providerId).toBe("deepgram");
217
+ expect(transcriber!.boundaryId).toBe("daemon-batch");
218
+ });
219
+
220
+ test("returns null when deepgram is configured but no credentials exist", async () => {
221
+ mockProviderKeys = {}; // no keys
222
+ mockConfig = buildConfig({ provider: "deepgram" });
223
+
224
+ const transcriber = await resolveBatchTranscriber();
225
+
226
+ expect(transcriber).toBeNull();
227
+ });
228
+
229
+ test("deepgram uses 'deepgram' credential key, not 'openai'", async () => {
230
+ // Only openai key is set — deepgram should NOT resolve
231
+ mockProviderKeys["openai"] = "sk-test-key";
232
+ mockConfig = buildConfig({ provider: "deepgram" });
233
+
234
+ const transcriber = await resolveBatchTranscriber();
235
+
236
+ expect(transcriber).toBeNull();
237
+ });
238
+
239
+ test("resolved deepgram transcriber has stable provider identity", async () => {
240
+ mockProviderKeys["deepgram"] = "dg-identity-test";
241
+ mockConfig = buildConfig({ provider: "deepgram" });
242
+
243
+ const transcriber = await resolveBatchTranscriber();
244
+
245
+ expect(transcriber!.providerId).toBe("deepgram");
246
+ expect(transcriber!.boundaryId).toBe("daemon-batch");
247
+ });
248
+
249
+ // -------------------------------------------------------------------------
250
+ // Google Gemini provider resolution
251
+ // -------------------------------------------------------------------------
252
+
253
+ test("returns a BatchTranscriber when google-gemini is configured and credentials are available", async () => {
254
+ mockProviderKeys["gemini"] = "gemini-test-key";
255
+ mockConfig = buildConfig({ provider: "google-gemini" });
256
+
257
+ const transcriber = await resolveBatchTranscriber();
258
+
259
+ expect(transcriber).not.toBeNull();
260
+ expect(transcriber!.providerId).toBe("google-gemini");
261
+ expect(transcriber!.boundaryId).toBe("daemon-batch");
262
+ });
263
+
264
+ test("returns null when google-gemini is configured but no credentials exist", async () => {
265
+ mockProviderKeys = {}; // no keys
266
+ mockConfig = buildConfig({ provider: "google-gemini" });
267
+
268
+ const transcriber = await resolveBatchTranscriber();
269
+
270
+ expect(transcriber).toBeNull();
271
+ });
272
+
273
+ test("google-gemini uses 'gemini' credential key, not 'openai' or 'deepgram'", async () => {
274
+ // Only openai key is set — google-gemini should NOT resolve
275
+ mockProviderKeys["openai"] = "sk-test-key";
276
+ mockConfig = buildConfig({ provider: "google-gemini" });
277
+
278
+ const transcriber = await resolveBatchTranscriber();
279
+
280
+ expect(transcriber).toBeNull();
281
+ });
282
+
283
+ test("resolved google-gemini transcriber has stable provider identity", async () => {
284
+ mockProviderKeys["gemini"] = "gemini-identity-test";
285
+ mockConfig = buildConfig({ provider: "google-gemini" });
286
+
287
+ const transcriber = await resolveBatchTranscriber();
288
+
289
+ expect(transcriber!.providerId).toBe("google-gemini");
290
+ expect(transcriber!.boundaryId).toBe("daemon-batch");
291
+ });
292
+ });
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Tests — resolveTelephonySttCapability
296
+ // ---------------------------------------------------------------------------
297
+
298
+ describe("resolveTelephonySttCapability", () => {
299
+ beforeEach(() => {
300
+ mockConfig = buildConfig({});
301
+ mockProviderKeys = {};
302
+ });
303
+
304
+ test("returns 'supported' when provider is telephony-eligible and credentials exist", async () => {
305
+ mockProviderKeys["openai"] = "sk-telephony-test";
306
+ mockConfig = buildConfig({ provider: "openai-whisper" });
307
+
308
+ const result = await resolveTelephonySttCapability();
309
+
310
+ expect(result.status).toBe("supported");
311
+ if (result.status === "supported") {
312
+ expect(result.providerId).toBe("openai-whisper");
313
+ // openai-whisper is batch-only, so telephonyMode should reflect that
314
+ expect(result.telephonyMode).toBe("batch-only");
315
+ }
316
+ });
317
+
318
+ test("returns 'unconfigured' when provider is not in the catalog", async () => {
319
+ mockProviderKeys["unknown-provider"] = "key-doesnt-matter";
320
+ mockConfig = buildConfig({ provider: "unknown-provider" as string });
321
+
322
+ const result = await resolveTelephonySttCapability();
323
+
324
+ expect(result.status).toBe("unconfigured");
325
+ if (result.status === "unconfigured") {
326
+ expect(result.reason).toContain("unknown-provider");
327
+ expect(result.reason).toContain("not in the provider catalog");
328
+ }
329
+ });
330
+
331
+ test("returns 'missing-credentials' when provider is eligible but has no API key", async () => {
332
+ mockProviderKeys = {}; // no keys
333
+ mockConfig = buildConfig({ provider: "openai-whisper" });
334
+
335
+ const result = await resolveTelephonySttCapability();
336
+
337
+ expect(result.status).toBe("missing-credentials");
338
+ if (result.status === "missing-credentials") {
339
+ expect(result.providerId).toBe("openai-whisper");
340
+ expect(result.credentialProvider).toBe("openai");
341
+ expect(result.reason).toContain("openai");
342
+ }
343
+ });
344
+
345
+ test("uses config-driven provider, not a hardcoded default", async () => {
346
+ // Use a provider that IS in the catalog to verify config is read
347
+ mockProviderKeys["openai"] = "sk-config-test";
348
+ mockConfig = buildConfig({ provider: "openai-whisper" });
349
+
350
+ const result = await resolveTelephonySttCapability();
351
+
352
+ expect(result.status).toBe("supported");
353
+ if (result.status === "supported") {
354
+ expect(result.providerId).toBe("openai-whisper");
355
+ }
356
+ });
357
+
358
+ test("returns 'unconfigured' for empty-string provider", async () => {
359
+ mockConfig = buildConfig({ provider: "" as string });
360
+
361
+ const result = await resolveTelephonySttCapability();
362
+
363
+ expect(result.status).toBe("unconfigured");
364
+ });
365
+
366
+ // -------------------------------------------------------------------------
367
+ // Google Gemini telephony capability
368
+ // -------------------------------------------------------------------------
369
+
370
+ test("returns 'supported' for google-gemini with batch-only telephonyMode", async () => {
371
+ mockProviderKeys["gemini"] = "gemini-telephony-test";
372
+ mockConfig = buildConfig({ provider: "google-gemini" });
373
+
374
+ const result = await resolveTelephonySttCapability();
375
+
376
+ expect(result.status).toBe("supported");
377
+ if (result.status === "supported") {
378
+ expect(result.providerId).toBe("google-gemini");
379
+ expect(result.telephonyMode).toBe("batch-only");
380
+ }
381
+ });
382
+
383
+ test("returns 'missing-credentials' for google-gemini without a gemini key", async () => {
384
+ mockProviderKeys = {};
385
+ mockConfig = buildConfig({ provider: "google-gemini" });
386
+
387
+ const result = await resolveTelephonySttCapability();
388
+
389
+ expect(result.status).toBe("missing-credentials");
390
+ if (result.status === "missing-credentials") {
391
+ expect(result.providerId).toBe("google-gemini");
392
+ expect(result.credentialProvider).toBe("gemini");
393
+ }
394
+ });
395
+ });
396
+
397
+ // ---------------------------------------------------------------------------
398
+ // Tests — telephony routing alignment with provider catalog
399
+ // ---------------------------------------------------------------------------
400
+
401
+ import { getProviderEntry, listProviderIds } from "../provider-catalog.js";
402
+
403
+ describe("telephony routing catalog alignment", () => {
404
+ /**
405
+ * These tests verify that the assumptions made by the telephony STT
406
+ * routing resolver (telephony-stt-routing.ts) remain consistent with
407
+ * the provider catalog entries. If a catalog entry changes its
408
+ * telephonyMode, routing metadata, or a new provider is added, these
409
+ * tests will catch misalignment early.
410
+ */
411
+
412
+ test("deepgram catalog entry has realtime-ws telephonyMode (Twilio-native eligible)", () => {
413
+ const entry = getProviderEntry("deepgram");
414
+ expect(entry).toBeDefined();
415
+ expect(entry!.telephonyMode).toBe("realtime-ws");
416
+ });
417
+
418
+ test("google-gemini catalog entry has batch-only telephonyMode (Twilio-native eligible)", () => {
419
+ const entry = getProviderEntry("google-gemini");
420
+ expect(entry).toBeDefined();
421
+ expect(entry!.telephonyMode).toBe("batch-only");
422
+ });
423
+
424
+ test("openai-whisper catalog entry has batch-only telephonyMode (media-stream path)", () => {
425
+ const entry = getProviderEntry("openai-whisper");
426
+ expect(entry).toBeDefined();
427
+ expect(entry!.telephonyMode).toBe("batch-only");
428
+ });
429
+
430
+ test("deepgram uses 'deepgram' credential provider", () => {
431
+ const entry = getProviderEntry("deepgram");
432
+ expect(entry!.credentialProvider).toBe("deepgram");
433
+ });
434
+
435
+ test("google-gemini uses 'gemini' credential provider", () => {
436
+ const entry = getProviderEntry("google-gemini");
437
+ expect(entry!.credentialProvider).toBe("gemini");
438
+ });
439
+
440
+ test("openai-whisper uses 'openai' credential provider", () => {
441
+ const entry = getProviderEntry("openai-whisper");
442
+ expect(entry!.credentialProvider).toBe("openai");
443
+ });
444
+
445
+ test("every catalog provider has a non-none telephonyMode", () => {
446
+ // The telephony routing resolver assumes all known providers
447
+ // participate in some telephony path (native or media-stream).
448
+ // If a provider with telephonyMode: "none" is added, the routing
449
+ // resolver would need to handle it explicitly.
450
+ for (const id of listProviderIds()) {
451
+ const entry = getProviderEntry(id);
452
+ expect(entry).toBeDefined();
453
+ expect(entry!.telephonyMode).not.toBe("none");
454
+ }
455
+ });
456
+
457
+ // -----------------------------------------------------------------------
458
+ // Telephony routing metadata invariants
459
+ // -----------------------------------------------------------------------
460
+
461
+ test("every catalog provider has telephonyRouting metadata", () => {
462
+ for (const id of listProviderIds()) {
463
+ const entry = getProviderEntry(id);
464
+ expect(entry).toBeDefined();
465
+ expect(entry!.telephonyRouting).toBeDefined();
466
+ expect(["conversation-relay-native", "media-stream-custom"]).toContain(
467
+ entry!.telephonyRouting.strategyKind,
468
+ );
469
+ }
470
+ });
471
+
472
+ test("conversation-relay-native providers have twilioNativeMapping with non-empty provider name", () => {
473
+ for (const id of listProviderIds()) {
474
+ const entry = getProviderEntry(id)!;
475
+ if (entry.telephonyRouting.strategyKind === "conversation-relay-native") {
476
+ expect(entry.telephonyRouting.twilioNativeMapping).toBeDefined();
477
+ expect(
478
+ entry.telephonyRouting.twilioNativeMapping!.provider.length,
479
+ ).toBeGreaterThan(0);
480
+ }
481
+ }
482
+ });
483
+
484
+ test("media-stream-custom providers do not have twilioNativeMapping", () => {
485
+ for (const id of listProviderIds()) {
486
+ const entry = getProviderEntry(id)!;
487
+ if (entry.telephonyRouting.strategyKind === "media-stream-custom") {
488
+ expect(entry.telephonyRouting.twilioNativeMapping).toBeUndefined();
489
+ }
490
+ }
491
+ });
492
+
493
+ test("deepgram routing metadata maps to Twilio-native Deepgram with nova-3 speech model", () => {
494
+ const entry = getProviderEntry("deepgram")!;
495
+ expect(entry.telephonyRouting.strategyKind).toBe(
496
+ "conversation-relay-native",
497
+ );
498
+ expect(entry.telephonyRouting.twilioNativeMapping?.provider).toBe(
499
+ "Deepgram",
500
+ );
501
+ expect(entry.telephonyRouting.twilioNativeMapping?.defaultSpeechModel).toBe(
502
+ "nova-3",
503
+ );
504
+ });
505
+
506
+ test("google-gemini routing metadata maps to Twilio-native Google with no default speech model", () => {
507
+ const entry = getProviderEntry("google-gemini")!;
508
+ expect(entry.telephonyRouting.strategyKind).toBe(
509
+ "conversation-relay-native",
510
+ );
511
+ expect(entry.telephonyRouting.twilioNativeMapping?.provider).toBe("Google");
512
+ expect(
513
+ entry.telephonyRouting.twilioNativeMapping?.defaultSpeechModel,
514
+ ).toBeUndefined();
515
+ });
516
+
517
+ test("openai-whisper routing metadata uses media-stream-custom without Twilio mapping", () => {
518
+ const entry = getProviderEntry("openai-whisper")!;
519
+ expect(entry.telephonyRouting.strategyKind).toBe("media-stream-custom");
520
+ expect(entry.telephonyRouting.twilioNativeMapping).toBeUndefined();
521
+ });
522
+
523
+ // -----------------------------------------------------------------------
524
+ // Stable provider identity
525
+ // -----------------------------------------------------------------------
526
+
527
+ test("provider IDs remain stable across catalog lookups", () => {
528
+ // Guard against accidental ID mutation or aliasing bugs.
529
+ for (const id of listProviderIds()) {
530
+ const entry = getProviderEntry(id);
531
+ expect(entry).toBeDefined();
532
+ expect(entry!.id).toBe(id);
533
+ }
534
+ });
535
+
536
+ test("capability resolver returns supported for all catalog providers with credentials", async () => {
537
+ // Verify that every provider in the catalog can resolve to "supported"
538
+ // when the correct credentials are present. This catches regressions
539
+ // where a catalog entry is added but the credential mapping is wrong.
540
+ const credentialMap: Record<string, string> = {
541
+ "openai-whisper": "openai",
542
+ deepgram: "deepgram",
543
+ "google-gemini": "gemini",
544
+ };
545
+
546
+ for (const id of listProviderIds()) {
547
+ const credKey = credentialMap[id];
548
+ expect(credKey).toBeDefined();
549
+
550
+ mockProviderKeys = { [credKey]: `test-key-${id}` };
551
+ mockConfig = buildConfig({ provider: id });
552
+
553
+ const result = await resolveTelephonySttCapability();
554
+ expect(result.status).toBe("supported");
555
+ if (result.status === "supported") {
556
+ expect(result.providerId).toBe(id);
557
+ }
558
+ }
559
+ });
560
+ });
561
+
562
+ // ---------------------------------------------------------------------------
563
+ // Tests — resolveConversationStreamingSttCapability
564
+ // ---------------------------------------------------------------------------
565
+
566
+ describe("resolveConversationStreamingSttCapability", () => {
567
+ beforeEach(() => {
568
+ mockConfig = buildConfig({});
569
+ mockProviderKeys = {};
570
+ });
571
+
572
+ // -------------------------------------------------------------------------
573
+ // Deepgram — realtime-ws streaming
574
+ // -------------------------------------------------------------------------
575
+
576
+ test("returns 'supported' with realtime-ws mode for deepgram", async () => {
577
+ mockProviderKeys["deepgram"] = "dg-stream-key";
578
+ mockConfig = buildConfig({ provider: "deepgram" });
579
+
580
+ const result = await resolveConversationStreamingSttCapability();
581
+
582
+ expect(result.status).toBe("supported");
583
+ if (result.status === "supported") {
584
+ expect(result.providerId).toBe("deepgram");
585
+ expect(result.streamingMode).toBe("realtime-ws");
586
+ }
587
+ });
588
+
589
+ test("returns 'missing-credentials' for deepgram without an API key", async () => {
590
+ mockProviderKeys = {};
591
+ mockConfig = buildConfig({ provider: "deepgram" });
592
+
593
+ const result = await resolveConversationStreamingSttCapability();
594
+
595
+ expect(result.status).toBe("missing-credentials");
596
+ if (result.status === "missing-credentials") {
597
+ expect(result.providerId).toBe("deepgram");
598
+ expect(result.credentialProvider).toBe("deepgram");
599
+ expect(result.reason).toContain("deepgram");
600
+ }
601
+ });
602
+
603
+ // -------------------------------------------------------------------------
604
+ // Google Gemini — realtime-ws streaming (Live API)
605
+ // -------------------------------------------------------------------------
606
+
607
+ test("returns 'supported' with realtime-ws mode for google-gemini", async () => {
608
+ mockProviderKeys["gemini"] = "gemini-stream-key";
609
+ mockConfig = buildConfig({ provider: "google-gemini" });
610
+
611
+ const result = await resolveConversationStreamingSttCapability();
612
+
613
+ expect(result.status).toBe("supported");
614
+ if (result.status === "supported") {
615
+ expect(result.providerId).toBe("google-gemini");
616
+ expect(result.streamingMode).toBe("realtime-ws");
617
+ }
618
+ });
619
+
620
+ test("returns 'missing-credentials' for google-gemini without a gemini key", async () => {
621
+ mockProviderKeys = {};
622
+ mockConfig = buildConfig({ provider: "google-gemini" });
623
+
624
+ const result = await resolveConversationStreamingSttCapability();
625
+
626
+ expect(result.status).toBe("missing-credentials");
627
+ if (result.status === "missing-credentials") {
628
+ expect(result.providerId).toBe("google-gemini");
629
+ expect(result.credentialProvider).toBe("gemini");
630
+ }
631
+ });
632
+
633
+ // -------------------------------------------------------------------------
634
+ // OpenAI Whisper — incremental-batch streaming
635
+ // -------------------------------------------------------------------------
636
+
637
+ test("returns 'supported' with incremental-batch mode for openai-whisper", async () => {
638
+ mockProviderKeys["openai"] = "sk-stream-test";
639
+ mockConfig = buildConfig({ provider: "openai-whisper" });
640
+
641
+ const result = await resolveConversationStreamingSttCapability();
642
+
643
+ expect(result.status).toBe("supported");
644
+ if (result.status === "supported") {
645
+ expect(result.providerId).toBe("openai-whisper");
646
+ expect(result.streamingMode).toBe("incremental-batch");
647
+ }
648
+ });
649
+
650
+ test("returns 'missing-credentials' for openai-whisper without an API key", async () => {
651
+ mockProviderKeys = {};
652
+ mockConfig = buildConfig({ provider: "openai-whisper" });
653
+
654
+ const result = await resolveConversationStreamingSttCapability();
655
+
656
+ expect(result.status).toBe("missing-credentials");
657
+ if (result.status === "missing-credentials") {
658
+ expect(result.providerId).toBe("openai-whisper");
659
+ expect(result.credentialProvider).toBe("openai");
660
+ expect(result.reason).toContain("openai");
661
+ }
662
+ });
663
+
664
+ // -------------------------------------------------------------------------
665
+ // Unknown / unconfigured provider
666
+ // -------------------------------------------------------------------------
667
+
668
+ test("returns 'unconfigured' when provider is not in the catalog", async () => {
669
+ mockProviderKeys["unknown-provider"] = "key-doesnt-matter";
670
+ mockConfig = buildConfig({ provider: "unknown-provider" as string });
671
+
672
+ const result = await resolveConversationStreamingSttCapability();
673
+
674
+ expect(result.status).toBe("unconfigured");
675
+ if (result.status === "unconfigured") {
676
+ expect(result.reason).toContain("unknown-provider");
677
+ expect(result.reason).toContain("not in the provider catalog");
678
+ }
679
+ });
680
+
681
+ test("returns 'unconfigured' for empty-string provider", async () => {
682
+ mockConfig = buildConfig({ provider: "" as string });
683
+
684
+ const result = await resolveConversationStreamingSttCapability();
685
+
686
+ expect(result.status).toBe("unconfigured");
687
+ });
688
+
689
+ // -------------------------------------------------------------------------
690
+ // Config-driven behaviour
691
+ // -------------------------------------------------------------------------
692
+
693
+ test("uses config-driven provider, not a hardcoded default", async () => {
694
+ mockProviderKeys["deepgram"] = "dg-config-test";
695
+ mockConfig = buildConfig({ provider: "deepgram" });
696
+
697
+ const result = await resolveConversationStreamingSttCapability();
698
+
699
+ expect(result.status).toBe("supported");
700
+ if (result.status === "supported") {
701
+ expect(result.providerId).toBe("deepgram");
702
+ }
703
+ });
704
+ });
705
+
706
+ // ---------------------------------------------------------------------------
707
+ // Tests — resolveStreamingTranscriber (diarize preference)
708
+ // ---------------------------------------------------------------------------
709
+
710
+ describe("resolveStreamingTranscriber diarize preference", () => {
711
+ beforeEach(() => {
712
+ mockConfig = buildConfig({});
713
+ mockProviderKeys = {};
714
+ deepgramCtorCalls.length = 0;
715
+ geminiCtorCalls.length = 0;
716
+ whisperCtorCalls.length = 0;
717
+ loggerWarnings.length = 0;
718
+ });
719
+
720
+ test("default (no diarize option) constructs Deepgram without the diarize flag", async () => {
721
+ mockProviderKeys["deepgram"] = "dg-key";
722
+ mockConfig = buildConfig({ provider: "deepgram" });
723
+
724
+ const transcriber = await resolveStreamingTranscriber();
725
+
726
+ expect(transcriber).not.toBeNull();
727
+ expect(deepgramCtorCalls).toHaveLength(1);
728
+ const options = deepgramCtorCalls[0]!.options as Record<string, unknown>;
729
+ expect(options).not.toHaveProperty("diarize");
730
+ });
731
+
732
+ test("diarize: 'off' constructs Deepgram without the diarize flag", async () => {
733
+ mockProviderKeys["deepgram"] = "dg-key";
734
+ mockConfig = buildConfig({ provider: "deepgram" });
735
+
736
+ const transcriber = await resolveStreamingTranscriber({ diarize: "off" });
737
+
738
+ expect(transcriber).not.toBeNull();
739
+ expect(deepgramCtorCalls).toHaveLength(1);
740
+ const options = deepgramCtorCalls[0]!.options as Record<string, unknown>;
741
+ expect(options).not.toHaveProperty("diarize");
742
+ });
743
+
744
+ test("diarize: 'preferred' with Deepgram constructs the transcriber with diarize: true", async () => {
745
+ mockProviderKeys["deepgram"] = "dg-key";
746
+ mockConfig = buildConfig({ provider: "deepgram" });
747
+
748
+ const transcriber = await resolveStreamingTranscriber({
749
+ diarize: "preferred",
750
+ });
751
+
752
+ expect(transcriber).not.toBeNull();
753
+ expect(deepgramCtorCalls).toHaveLength(1);
754
+ const options = deepgramCtorCalls[0]!.options as Record<string, unknown>;
755
+ expect(options.diarize).toBe(true);
756
+ });
757
+
758
+ test("diarize: 'preferred' with Gemini silently skips diarization (no error, no diarize arg)", async () => {
759
+ mockProviderKeys["gemini"] = "gemini-key";
760
+ mockConfig = buildConfig({ provider: "google-gemini" });
761
+
762
+ const transcriber = await resolveStreamingTranscriber({
763
+ diarize: "preferred",
764
+ });
765
+
766
+ expect(transcriber).not.toBeNull();
767
+ expect(geminiCtorCalls).toHaveLength(1);
768
+ const options = geminiCtorCalls[0]!.options as Record<string, unknown>;
769
+ // Gemini never receives a diarize option — the resolver silently skips.
770
+ expect(options).not.toHaveProperty("diarize");
771
+ // No warning is logged for the silent-skip path.
772
+ expect(loggerWarnings).toHaveLength(0);
773
+ });
774
+
775
+ test("diarize: 'required' with Gemini returns null and logs a warning identifying the provider", async () => {
776
+ mockProviderKeys["gemini"] = "gemini-key";
777
+ mockConfig = buildConfig({ provider: "google-gemini" });
778
+
779
+ const transcriber = await resolveStreamingTranscriber({
780
+ diarize: "required",
781
+ });
782
+
783
+ expect(transcriber).toBeNull();
784
+ // No provider constructor was invoked.
785
+ expect(geminiCtorCalls).toHaveLength(0);
786
+ expect(deepgramCtorCalls).toHaveLength(0);
787
+ expect(whisperCtorCalls).toHaveLength(0);
788
+ // A warning was logged that identifies the configured provider so
789
+ // operators can debug mis-configured diarization requirements.
790
+ expect(loggerWarnings).toHaveLength(1);
791
+ const warning = loggerWarnings[0]!;
792
+ expect(warning.message).toContain("diarization");
793
+ expect((warning.data as { providerId?: unknown }).providerId).toBe(
794
+ "google-gemini",
795
+ );
796
+ });
797
+
798
+ test("diarize: 'required' with Deepgram constructs the transcriber with diarize: true", async () => {
799
+ mockProviderKeys["deepgram"] = "dg-key";
800
+ mockConfig = buildConfig({ provider: "deepgram" });
801
+
802
+ const transcriber = await resolveStreamingTranscriber({
803
+ diarize: "required",
804
+ });
805
+
806
+ expect(transcriber).not.toBeNull();
807
+ expect(deepgramCtorCalls).toHaveLength(1);
808
+ const options = deepgramCtorCalls[0]!.options as Record<string, unknown>;
809
+ expect(options.diarize).toBe(true);
810
+ // No warning logged on the happy path.
811
+ expect(loggerWarnings).toHaveLength(0);
812
+ });
813
+
814
+ test("sampleRate is still forwarded when diarize is enabled", async () => {
815
+ mockProviderKeys["deepgram"] = "dg-key";
816
+ mockConfig = buildConfig({ provider: "deepgram" });
817
+
818
+ await resolveStreamingTranscriber({
819
+ diarize: "preferred",
820
+ sampleRate: 48000,
821
+ });
822
+
823
+ expect(deepgramCtorCalls).toHaveLength(1);
824
+ const options = deepgramCtorCalls[0]!.options as Record<string, unknown>;
825
+ expect(options.sampleRate).toBe(48000);
826
+ expect(options.diarize).toBe(true);
827
+ });
828
+ });