@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.
- package/ARCHITECTURE.md +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +5 -13
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/openapi.yaml +982 -72
- package/package.json +4 -6
- package/scripts/generate-openapi.ts +0 -1
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +11 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-schema.test.ts +1013 -66
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +98 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +901 -60
- package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/credential-vault-unit.test.ts +379 -3
- package/src/__tests__/credentials-cli.test.ts +40 -16
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/headless-browser-interactions.test.ts +43 -0
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +176 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +5 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +112 -26
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +18 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +43 -3
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +255 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/gmail/SKILL.md +53 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +2 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +34 -10
- package/src/config/feature-flag-registry.json +46 -14
- package/src/config/loader.ts +26 -12
- package/src/config/schema.ts +35 -10
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/services.ts +44 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +45 -12
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +101 -24
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +581 -19
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +11 -1
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +94 -4
- package/src/daemon/conversation-tool-setup.ts +25 -0
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation.ts +86 -28
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +4 -1
- package/src/daemon/handlers/shared.ts +22 -0
- package/src/daemon/handlers/skills.ts +321 -77
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +122 -25
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +34 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +14 -0
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +347 -2
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +99 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/db-init.ts +6 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +27 -18
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +9 -0
- package/src/oauth/platform-connection.test.ts +98 -0
- package/src/oauth/platform-connection.ts +52 -31
- package/src/oauth/seed-providers.ts +7 -0
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +59 -18
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -61
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +9 -0
- package/src/runtime/AGENTS.md +43 -1
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +30 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/http-server.ts +395 -10
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +34 -0
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +115 -0
- package/src/runtime/routes/conversation-routes.ts +367 -146
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +40 -5
- package/src/runtime/routes/settings-routes.ts +22 -5
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +50 -0
- package/src/security/oauth2.ts +26 -4
- package/src/security/secure-keys.ts +25 -2
- package/src/security/token-manager.ts +8 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +38 -14
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/browser-execution.ts +1163 -23
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/terminal/safe-env.ts +10 -2
- package/src/tools/terminal/shell.ts +15 -4
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +17 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +54 -10
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Module mocks — must appear before any imports of the modules under test
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
mock.module("../../util/logger.js", () => ({
|
|
8
|
+
getLogger: () =>
|
|
9
|
+
new Proxy({} as Record<string, unknown>, {
|
|
10
|
+
get: () => () => {},
|
|
11
|
+
}),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// -- Config mock -----------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
let mockElevenLabsConfig = {
|
|
17
|
+
voiceId: "test-voice-id",
|
|
18
|
+
voiceModelId: "",
|
|
19
|
+
speed: 1.0,
|
|
20
|
+
stability: 0.5,
|
|
21
|
+
similarityBoost: 0.75,
|
|
22
|
+
conversationTimeoutSeconds: 30,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let mockFishAudioConfig = {
|
|
26
|
+
referenceId: "test-reference-id",
|
|
27
|
+
chunkLength: 200,
|
|
28
|
+
format: "mp3" as "mp3" | "wav" | "opus",
|
|
29
|
+
latency: "normal" as "normal" | "balanced",
|
|
30
|
+
speed: 1.0,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let mockDeepgramConfig = {
|
|
34
|
+
model: "aura-asteria-en",
|
|
35
|
+
format: "mp3" as "mp3" | "wav" | "opus",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
mock.module("../../config/loader.js", () => ({
|
|
39
|
+
getConfig: () => ({
|
|
40
|
+
services: {
|
|
41
|
+
tts: {
|
|
42
|
+
providers: {
|
|
43
|
+
elevenlabs: mockElevenLabsConfig,
|
|
44
|
+
"fish-audio": mockFishAudioConfig,
|
|
45
|
+
deepgram: mockDeepgramConfig,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// -- Secure keys mock ------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
let mockApiKey: string | null = "test-elevenlabs-api-key";
|
|
55
|
+
let mockDeepgramApiKey: string | null = "test-deepgram-api-key";
|
|
56
|
+
|
|
57
|
+
mock.module("../../security/secure-keys.js", () => ({
|
|
58
|
+
getSecureKeyAsync: async () => mockApiKey,
|
|
59
|
+
getProviderKeyAsync: async (provider: string) => {
|
|
60
|
+
if (provider === "deepgram") return mockDeepgramApiKey;
|
|
61
|
+
return mockApiKey;
|
|
62
|
+
},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
mock.module("../../security/credential-key.js", () => ({
|
|
66
|
+
credentialKey: (service: string, field: string) =>
|
|
67
|
+
`credential/${service}/${field}`,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// -- Fish Audio client mock ------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const mockSynthesizeWithFishAudio = mock(
|
|
73
|
+
async (
|
|
74
|
+
_text: string,
|
|
75
|
+
_config: unknown,
|
|
76
|
+
options?: { onChunk?: (chunk: Uint8Array) => void; signal?: AbortSignal },
|
|
77
|
+
) => {
|
|
78
|
+
const audioData = Buffer.from("fake-fish-audio-data");
|
|
79
|
+
if (options?.onChunk) {
|
|
80
|
+
options.onChunk(new Uint8Array(audioData));
|
|
81
|
+
}
|
|
82
|
+
return audioData;
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
mock.module("../../calls/fish-audio-client.js", () => ({
|
|
87
|
+
synthesizeWithFishAudio: mockSynthesizeWithFishAudio,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Imports (after mocks)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
import { listCatalogProviderIds } from "../provider-catalog.js";
|
|
95
|
+
import {
|
|
96
|
+
_resetTtsProviderRegistry,
|
|
97
|
+
getTtsProvider,
|
|
98
|
+
listTtsProviders,
|
|
99
|
+
} from "../provider-registry.js";
|
|
100
|
+
import {
|
|
101
|
+
createDeepgramProvider,
|
|
102
|
+
DeepgramTtsError,
|
|
103
|
+
} from "../providers/deepgram-provider.js";
|
|
104
|
+
import { createElevenLabsProvider } from "../providers/elevenlabs-provider.js";
|
|
105
|
+
import { ElevenLabsTtsError } from "../providers/elevenlabs-provider.js";
|
|
106
|
+
import { createFishAudioProvider } from "../providers/fish-audio-provider.js";
|
|
107
|
+
import { FishAudioTtsError } from "../providers/fish-audio-provider.js";
|
|
108
|
+
import { providerFactories } from "../providers/index.js";
|
|
109
|
+
import {
|
|
110
|
+
_resetBuiltinRegistration,
|
|
111
|
+
registerBuiltinTtsProviders,
|
|
112
|
+
} from "../providers/register-builtins.js";
|
|
113
|
+
import type { TtsSynthesisRequest } from "../types.js";
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Fetch mock helpers
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
let originalFetch: typeof globalThis.fetch;
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
originalFetch = globalThis.fetch;
|
|
123
|
+
mockApiKey = "test-elevenlabs-api-key";
|
|
124
|
+
mockDeepgramApiKey = "test-deepgram-api-key";
|
|
125
|
+
mockElevenLabsConfig = {
|
|
126
|
+
voiceId: "test-voice-id",
|
|
127
|
+
voiceModelId: "",
|
|
128
|
+
speed: 1.0,
|
|
129
|
+
stability: 0.5,
|
|
130
|
+
similarityBoost: 0.75,
|
|
131
|
+
conversationTimeoutSeconds: 30,
|
|
132
|
+
};
|
|
133
|
+
mockFishAudioConfig = {
|
|
134
|
+
referenceId: "test-reference-id",
|
|
135
|
+
chunkLength: 200,
|
|
136
|
+
format: "mp3",
|
|
137
|
+
latency: "normal",
|
|
138
|
+
speed: 1.0,
|
|
139
|
+
};
|
|
140
|
+
mockDeepgramConfig = {
|
|
141
|
+
model: "aura-asteria-en",
|
|
142
|
+
format: "mp3",
|
|
143
|
+
};
|
|
144
|
+
mockSynthesizeWithFishAudio.mockClear();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
globalThis.fetch = originalFetch;
|
|
149
|
+
_resetTtsProviderRegistry();
|
|
150
|
+
_resetBuiltinRegistration();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Helpers
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
function makeRequest(
|
|
158
|
+
overrides?: Partial<TtsSynthesisRequest>,
|
|
159
|
+
): TtsSynthesisRequest {
|
|
160
|
+
return {
|
|
161
|
+
text: "Hello world",
|
|
162
|
+
useCase: "message-playback",
|
|
163
|
+
...overrides,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function mockFetchReturning(audioBytes: Uint8Array, status = 200): void {
|
|
168
|
+
globalThis.fetch = mock(
|
|
169
|
+
async () =>
|
|
170
|
+
new Response(audioBytes.buffer as ArrayBuffer, {
|
|
171
|
+
status,
|
|
172
|
+
headers: { "Content-Type": "audio/mpeg" },
|
|
173
|
+
}),
|
|
174
|
+
) as unknown as typeof globalThis.fetch;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mockFetchError(status: number, body: string): void {
|
|
178
|
+
globalThis.fetch = mock(
|
|
179
|
+
async () => new Response(body, { status }),
|
|
180
|
+
) as unknown as typeof globalThis.fetch;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ===========================================================================
|
|
184
|
+
// ElevenLabs provider adapter
|
|
185
|
+
// ===========================================================================
|
|
186
|
+
|
|
187
|
+
describe("ElevenLabs TTS provider adapter", () => {
|
|
188
|
+
// -- Interface conformance -----------------------------------------------
|
|
189
|
+
|
|
190
|
+
test("has correct provider ID", () => {
|
|
191
|
+
const provider = createElevenLabsProvider();
|
|
192
|
+
expect(provider.id).toBe("elevenlabs");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("advertises mp3 and pcm format support without streaming", () => {
|
|
196
|
+
const provider = createElevenLabsProvider();
|
|
197
|
+
expect(provider.capabilities.supportsStreaming).toBe(false);
|
|
198
|
+
expect(provider.capabilities.supportedFormats).toEqual(["mp3", "pcm"]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// -- Request mapping -----------------------------------------------------
|
|
202
|
+
|
|
203
|
+
test("synthesize sends request to ElevenLabs REST API with correct voice ID", async () => {
|
|
204
|
+
const audioPayload = new Uint8Array([0x49, 0x44, 0x33]); // Fake MP3 header
|
|
205
|
+
let capturedUrl = "";
|
|
206
|
+
let capturedHeaders: Headers | null = null;
|
|
207
|
+
let capturedBody = "";
|
|
208
|
+
|
|
209
|
+
globalThis.fetch = mock(
|
|
210
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
211
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
212
|
+
capturedHeaders = new Headers(init?.headers);
|
|
213
|
+
capturedBody = init?.body as string;
|
|
214
|
+
return new Response(audioPayload, {
|
|
215
|
+
status: 200,
|
|
216
|
+
headers: { "Content-Type": "audio/mpeg" },
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
) as unknown as typeof globalThis.fetch;
|
|
220
|
+
|
|
221
|
+
const provider = createElevenLabsProvider();
|
|
222
|
+
await provider.synthesize(makeRequest());
|
|
223
|
+
|
|
224
|
+
expect(capturedUrl).toContain("/v1/text-to-speech/test-voice-id");
|
|
225
|
+
expect(capturedUrl).toContain("output_format=mp3_44100_128");
|
|
226
|
+
expect(capturedHeaders!.get("xi-api-key")).toBe("test-elevenlabs-api-key");
|
|
227
|
+
expect(capturedHeaders!.get("Content-Type")).toBe("application/json");
|
|
228
|
+
|
|
229
|
+
const body = JSON.parse(capturedBody);
|
|
230
|
+
expect(body.text).toBe("Hello world");
|
|
231
|
+
expect(body.voice_settings).toEqual({
|
|
232
|
+
stability: 0.5,
|
|
233
|
+
similarity_boost: 0.75,
|
|
234
|
+
speed: 1.0,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("uses lower-quality format for phone-call use case", async () => {
|
|
239
|
+
const audioPayload = new Uint8Array([0x49, 0x44, 0x33]);
|
|
240
|
+
let capturedUrl = "";
|
|
241
|
+
|
|
242
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
243
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
244
|
+
return new Response(audioPayload, { status: 200 });
|
|
245
|
+
}) as unknown as typeof globalThis.fetch;
|
|
246
|
+
|
|
247
|
+
const provider = createElevenLabsProvider();
|
|
248
|
+
await provider.synthesize(makeRequest({ useCase: "phone-call" }));
|
|
249
|
+
|
|
250
|
+
expect(capturedUrl).toContain("output_format=mp3_22050_32");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("request voiceId overrides config voiceId", async () => {
|
|
254
|
+
const audioPayload = new Uint8Array([0x49, 0x44, 0x33]);
|
|
255
|
+
let capturedUrl = "";
|
|
256
|
+
|
|
257
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
258
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
259
|
+
return new Response(audioPayload, { status: 200 });
|
|
260
|
+
}) as unknown as typeof globalThis.fetch;
|
|
261
|
+
|
|
262
|
+
const provider = createElevenLabsProvider();
|
|
263
|
+
await provider.synthesize(makeRequest({ voiceId: "override-voice" }));
|
|
264
|
+
|
|
265
|
+
expect(capturedUrl).toContain("/v1/text-to-speech/override-voice");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("uses configured voiceModelId when set", async () => {
|
|
269
|
+
mockElevenLabsConfig.voiceModelId = "eleven_turbo_v2_5";
|
|
270
|
+
|
|
271
|
+
const audioPayload = new Uint8Array([0x49, 0x44, 0x33]);
|
|
272
|
+
let capturedBody = "";
|
|
273
|
+
|
|
274
|
+
globalThis.fetch = mock(
|
|
275
|
+
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
276
|
+
capturedBody = init?.body as string;
|
|
277
|
+
return new Response(audioPayload, { status: 200 });
|
|
278
|
+
},
|
|
279
|
+
) as unknown as typeof globalThis.fetch;
|
|
280
|
+
|
|
281
|
+
const provider = createElevenLabsProvider();
|
|
282
|
+
await provider.synthesize(makeRequest());
|
|
283
|
+
|
|
284
|
+
const body = JSON.parse(capturedBody);
|
|
285
|
+
expect(body.model_id).toBe("eleven_turbo_v2_5");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// -- Content type / format -----------------------------------------------
|
|
289
|
+
|
|
290
|
+
test("returns audio/mpeg content type for mp3 format", async () => {
|
|
291
|
+
const audioPayload = new Uint8Array([0x49, 0x44, 0x33]);
|
|
292
|
+
mockFetchReturning(audioPayload);
|
|
293
|
+
|
|
294
|
+
const provider = createElevenLabsProvider();
|
|
295
|
+
const result = await provider.synthesize(makeRequest());
|
|
296
|
+
|
|
297
|
+
expect(result.contentType).toBe("audio/mpeg");
|
|
298
|
+
expect(result.audio.byteLength).toBeGreaterThan(0);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// -- Required config validation ------------------------------------------
|
|
302
|
+
|
|
303
|
+
test("throws ELEVENLABS_TTS_NO_API_KEY when API key is missing", async () => {
|
|
304
|
+
mockApiKey = null;
|
|
305
|
+
|
|
306
|
+
const provider = createElevenLabsProvider();
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
await provider.synthesize(makeRequest());
|
|
310
|
+
throw new Error("Expected synthesize to throw");
|
|
311
|
+
} catch (err) {
|
|
312
|
+
expect(err).toBeInstanceOf(ElevenLabsTtsError);
|
|
313
|
+
expect((err as ElevenLabsTtsError).code).toBe(
|
|
314
|
+
"ELEVENLABS_TTS_NO_API_KEY",
|
|
315
|
+
);
|
|
316
|
+
expect((err as ElevenLabsTtsError).message).toContain(
|
|
317
|
+
"API key not configured",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// -- Error handling ------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
test("throws ELEVENLABS_TTS_HTTP_ERROR on non-200 response", async () => {
|
|
325
|
+
mockFetchError(401, "Unauthorized");
|
|
326
|
+
|
|
327
|
+
const provider = createElevenLabsProvider();
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
await provider.synthesize(makeRequest());
|
|
331
|
+
throw new Error("Expected synthesize to throw");
|
|
332
|
+
} catch (err) {
|
|
333
|
+
expect(err).toBeInstanceOf(ElevenLabsTtsError);
|
|
334
|
+
expect((err as ElevenLabsTtsError).code).toBe(
|
|
335
|
+
"ELEVENLABS_TTS_HTTP_ERROR",
|
|
336
|
+
);
|
|
337
|
+
expect((err as ElevenLabsTtsError).statusCode).toBe(401);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("throws ELEVENLABS_TTS_EMPTY_RESPONSE on empty audio body", async () => {
|
|
342
|
+
mockFetchReturning(new Uint8Array(0));
|
|
343
|
+
|
|
344
|
+
const provider = createElevenLabsProvider();
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
await provider.synthesize(makeRequest());
|
|
348
|
+
throw new Error("Expected synthesize to throw");
|
|
349
|
+
} catch (err) {
|
|
350
|
+
expect(err).toBeInstanceOf(ElevenLabsTtsError);
|
|
351
|
+
expect((err as ElevenLabsTtsError).code).toBe(
|
|
352
|
+
"ELEVENLABS_TTS_EMPTY_RESPONSE",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("throws ELEVENLABS_TTS_REQUEST_FAILED on network error", async () => {
|
|
358
|
+
globalThis.fetch = mock(async () => {
|
|
359
|
+
throw new Error("Network unreachable");
|
|
360
|
+
}) as unknown as typeof globalThis.fetch;
|
|
361
|
+
|
|
362
|
+
const provider = createElevenLabsProvider();
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
await provider.synthesize(makeRequest());
|
|
366
|
+
throw new Error("Expected synthesize to throw");
|
|
367
|
+
} catch (err) {
|
|
368
|
+
expect(err).toBeInstanceOf(ElevenLabsTtsError);
|
|
369
|
+
expect((err as ElevenLabsTtsError).code).toBe(
|
|
370
|
+
"ELEVENLABS_TTS_REQUEST_FAILED",
|
|
371
|
+
);
|
|
372
|
+
expect((err as ElevenLabsTtsError).message).toContain(
|
|
373
|
+
"Network unreachable",
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ===========================================================================
|
|
380
|
+
// Fish Audio TTS provider adapter
|
|
381
|
+
// ===========================================================================
|
|
382
|
+
|
|
383
|
+
describe("Fish Audio TTS provider adapter", () => {
|
|
384
|
+
// -- Interface conformance -----------------------------------------------
|
|
385
|
+
|
|
386
|
+
test("has correct provider ID", () => {
|
|
387
|
+
const provider = createFishAudioProvider();
|
|
388
|
+
expect(provider.id).toBe("fish-audio");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("advertises streaming support with multiple formats", () => {
|
|
392
|
+
const provider = createFishAudioProvider();
|
|
393
|
+
expect(provider.capabilities.supportsStreaming).toBe(true);
|
|
394
|
+
expect(provider.capabilities.supportedFormats).toEqual([
|
|
395
|
+
"mp3",
|
|
396
|
+
"wav",
|
|
397
|
+
"opus",
|
|
398
|
+
]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("implements synthesizeStream", () => {
|
|
402
|
+
const provider = createFishAudioProvider();
|
|
403
|
+
expect(typeof provider.synthesizeStream).toBe("function");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// -- Request mapping -----------------------------------------------------
|
|
407
|
+
|
|
408
|
+
test("synthesize passes text and config to underlying client", async () => {
|
|
409
|
+
const provider = createFishAudioProvider();
|
|
410
|
+
await provider.synthesize(makeRequest({ text: "Test speech" }));
|
|
411
|
+
|
|
412
|
+
expect(mockSynthesizeWithFishAudio).toHaveBeenCalledTimes(1);
|
|
413
|
+
const [text, config, options] = mockSynthesizeWithFishAudio.mock.calls[0]!;
|
|
414
|
+
expect(text).toBe("Test speech");
|
|
415
|
+
expect((config as { referenceId: string }).referenceId).toBe(
|
|
416
|
+
"test-reference-id",
|
|
417
|
+
);
|
|
418
|
+
expect(
|
|
419
|
+
(options as { signal?: AbortSignal } | undefined)?.signal,
|
|
420
|
+
).toBeUndefined();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("request voiceId overrides config referenceId", async () => {
|
|
424
|
+
const provider = createFishAudioProvider();
|
|
425
|
+
await provider.synthesize(makeRequest({ voiceId: "custom-ref-id" }));
|
|
426
|
+
|
|
427
|
+
const [, config] = mockSynthesizeWithFishAudio.mock.calls[0]!;
|
|
428
|
+
expect((config as { referenceId: string }).referenceId).toBe(
|
|
429
|
+
"custom-ref-id",
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("passes abort signal to underlying client", async () => {
|
|
434
|
+
const controller = new AbortController();
|
|
435
|
+
const provider = createFishAudioProvider();
|
|
436
|
+
await provider.synthesize(makeRequest({ signal: controller.signal }));
|
|
437
|
+
|
|
438
|
+
const [, , options] = mockSynthesizeWithFishAudio.mock.calls[0]!;
|
|
439
|
+
expect((options as { signal?: AbortSignal } | undefined)?.signal).toBe(
|
|
440
|
+
controller.signal,
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// -- Streaming -----------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
test("synthesizeStream passes onChunk callback through", async () => {
|
|
447
|
+
const chunks: Uint8Array[] = [];
|
|
448
|
+
const provider = createFishAudioProvider();
|
|
449
|
+
await provider.synthesizeStream!(makeRequest(), (chunk) =>
|
|
450
|
+
chunks.push(chunk),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(mockSynthesizeWithFishAudio).toHaveBeenCalledTimes(1);
|
|
454
|
+
const [, , options] = mockSynthesizeWithFishAudio.mock.calls[0]!;
|
|
455
|
+
expect(typeof (options as { onChunk?: unknown } | undefined)?.onChunk).toBe(
|
|
456
|
+
"function",
|
|
457
|
+
);
|
|
458
|
+
// The mock calls onChunk once; verify it was received
|
|
459
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// -- Content type / format -----------------------------------------------
|
|
463
|
+
|
|
464
|
+
test("returns audio/mpeg content type for mp3 format", async () => {
|
|
465
|
+
mockFishAudioConfig.format = "mp3";
|
|
466
|
+
const provider = createFishAudioProvider();
|
|
467
|
+
const result = await provider.synthesize(makeRequest());
|
|
468
|
+
expect(result.contentType).toBe("audio/mpeg");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("returns audio/wav content type for wav format", async () => {
|
|
472
|
+
mockFishAudioConfig.format = "wav";
|
|
473
|
+
const provider = createFishAudioProvider();
|
|
474
|
+
const result = await provider.synthesize(makeRequest());
|
|
475
|
+
expect(result.contentType).toBe("audio/wav");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("returns audio/opus content type for opus format", async () => {
|
|
479
|
+
mockFishAudioConfig.format = "opus";
|
|
480
|
+
const provider = createFishAudioProvider();
|
|
481
|
+
const result = await provider.synthesize(makeRequest());
|
|
482
|
+
expect(result.contentType).toBe("audio/opus");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// -- Required config validation ------------------------------------------
|
|
486
|
+
|
|
487
|
+
test("throws FISH_AUDIO_TTS_NO_REFERENCE_ID when no reference ID is available", async () => {
|
|
488
|
+
mockFishAudioConfig.referenceId = "";
|
|
489
|
+
|
|
490
|
+
const provider = createFishAudioProvider();
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
await provider.synthesize(makeRequest());
|
|
494
|
+
throw new Error("Expected synthesize to throw");
|
|
495
|
+
} catch (err) {
|
|
496
|
+
expect(err).toBeInstanceOf(FishAudioTtsError);
|
|
497
|
+
expect((err as FishAudioTtsError).code).toBe(
|
|
498
|
+
"FISH_AUDIO_TTS_NO_REFERENCE_ID",
|
|
499
|
+
);
|
|
500
|
+
expect((err as FishAudioTtsError).message).toContain("reference ID");
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("throws FISH_AUDIO_TTS_NO_REFERENCE_ID in synthesizeStream when no reference ID", async () => {
|
|
505
|
+
mockFishAudioConfig.referenceId = "";
|
|
506
|
+
|
|
507
|
+
const provider = createFishAudioProvider();
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
await provider.synthesizeStream!(makeRequest(), () => {});
|
|
511
|
+
throw new Error("Expected synthesizeStream to throw");
|
|
512
|
+
} catch (err) {
|
|
513
|
+
expect(err).toBeInstanceOf(FishAudioTtsError);
|
|
514
|
+
expect((err as FishAudioTtsError).code).toBe(
|
|
515
|
+
"FISH_AUDIO_TTS_NO_REFERENCE_ID",
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// -- Error handling ------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
test("wraps underlying client errors with FISH_AUDIO_TTS_SYNTHESIS_FAILED", async () => {
|
|
523
|
+
mockSynthesizeWithFishAudio.mockImplementationOnce(async () => {
|
|
524
|
+
throw new Error("API key not configured");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const provider = createFishAudioProvider();
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
await provider.synthesize(makeRequest());
|
|
531
|
+
throw new Error("Expected synthesize to throw");
|
|
532
|
+
} catch (err) {
|
|
533
|
+
expect(err).toBeInstanceOf(FishAudioTtsError);
|
|
534
|
+
expect((err as FishAudioTtsError).code).toBe(
|
|
535
|
+
"FISH_AUDIO_TTS_SYNTHESIS_FAILED",
|
|
536
|
+
);
|
|
537
|
+
expect((err as FishAudioTtsError).message).toContain(
|
|
538
|
+
"API key not configured",
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("wraps streaming client errors with FISH_AUDIO_TTS_SYNTHESIS_FAILED", async () => {
|
|
544
|
+
mockSynthesizeWithFishAudio.mockImplementationOnce(async () => {
|
|
545
|
+
throw new Error("Connection reset");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const provider = createFishAudioProvider();
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
await provider.synthesizeStream!(makeRequest(), () => {});
|
|
552
|
+
throw new Error("Expected synthesizeStream to throw");
|
|
553
|
+
} catch (err) {
|
|
554
|
+
expect(err).toBeInstanceOf(FishAudioTtsError);
|
|
555
|
+
expect((err as FishAudioTtsError).code).toBe(
|
|
556
|
+
"FISH_AUDIO_TTS_SYNTHESIS_FAILED",
|
|
557
|
+
);
|
|
558
|
+
expect((err as FishAudioTtsError).message).toContain("Connection reset");
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ===========================================================================
|
|
564
|
+
// Deepgram TTS provider adapter
|
|
565
|
+
// ===========================================================================
|
|
566
|
+
|
|
567
|
+
describe("Deepgram TTS provider adapter", () => {
|
|
568
|
+
// -- Interface conformance -----------------------------------------------
|
|
569
|
+
|
|
570
|
+
test("has correct provider ID", () => {
|
|
571
|
+
const provider = createDeepgramProvider();
|
|
572
|
+
expect(provider.id).toBe("deepgram");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("advertises mp3, wav, opus format support without streaming", () => {
|
|
576
|
+
const provider = createDeepgramProvider();
|
|
577
|
+
expect(provider.capabilities.supportsStreaming).toBe(false);
|
|
578
|
+
expect(provider.capabilities.supportedFormats).toEqual([
|
|
579
|
+
"mp3",
|
|
580
|
+
"wav",
|
|
581
|
+
"opus",
|
|
582
|
+
]);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// -- Request mapping -----------------------------------------------------
|
|
586
|
+
|
|
587
|
+
test("synthesize sends request to Deepgram REST TTS API with correct model", async () => {
|
|
588
|
+
const audioPayload = new Uint8Array([0x49, 0x44, 0x33]);
|
|
589
|
+
let capturedUrl = "";
|
|
590
|
+
let capturedHeaders: Headers | null = null;
|
|
591
|
+
let capturedBody = "";
|
|
592
|
+
|
|
593
|
+
globalThis.fetch = mock(
|
|
594
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
595
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
596
|
+
capturedHeaders = new Headers(init?.headers);
|
|
597
|
+
capturedBody = init?.body as string;
|
|
598
|
+
return new Response(audioPayload, {
|
|
599
|
+
status: 200,
|
|
600
|
+
headers: { "Content-Type": "audio/mpeg" },
|
|
601
|
+
});
|
|
602
|
+
},
|
|
603
|
+
) as unknown as typeof globalThis.fetch;
|
|
604
|
+
|
|
605
|
+
const provider = createDeepgramProvider();
|
|
606
|
+
await provider.synthesize(makeRequest());
|
|
607
|
+
|
|
608
|
+
expect(capturedUrl).toContain("/v1/speak");
|
|
609
|
+
expect(capturedUrl).toContain("model=aura-asteria-en");
|
|
610
|
+
expect(capturedUrl).toContain("encoding=mp3");
|
|
611
|
+
expect(capturedHeaders!.get("Authorization")).toBe(
|
|
612
|
+
"Token test-deepgram-api-key",
|
|
613
|
+
);
|
|
614
|
+
expect(capturedHeaders!.get("Content-Type")).toBe("application/json");
|
|
615
|
+
|
|
616
|
+
const body = JSON.parse(capturedBody);
|
|
617
|
+
expect(body.text).toBe("Hello world");
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("uses linear16 encoding with container=none and sample_rate=16000 when outputFormat is pcm", async () => {
|
|
621
|
+
const audioPayload = new Uint8Array([0x00, 0x01, 0x02]);
|
|
622
|
+
let capturedUrl = "";
|
|
623
|
+
|
|
624
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
625
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
626
|
+
return new Response(audioPayload, { status: 200 });
|
|
627
|
+
}) as unknown as typeof globalThis.fetch;
|
|
628
|
+
|
|
629
|
+
const provider = createDeepgramProvider();
|
|
630
|
+
const result = await provider.synthesize(
|
|
631
|
+
makeRequest({ outputFormat: "pcm" }),
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
expect(capturedUrl).toContain("encoding=linear16");
|
|
635
|
+
expect(capturedUrl).toContain("container=none");
|
|
636
|
+
expect(capturedUrl).toContain("sample_rate=16000");
|
|
637
|
+
expect(result.contentType).toBe("audio/pcm");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("translates wav config format to linear16 encoding with container=wav", async () => {
|
|
641
|
+
mockDeepgramConfig.format = "wav";
|
|
642
|
+
const audioPayload = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
|
|
643
|
+
let capturedUrl = "";
|
|
644
|
+
|
|
645
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
646
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
647
|
+
return new Response(audioPayload, { status: 200 });
|
|
648
|
+
}) as unknown as typeof globalThis.fetch;
|
|
649
|
+
|
|
650
|
+
const provider = createDeepgramProvider();
|
|
651
|
+
const result = await provider.synthesize(makeRequest());
|
|
652
|
+
|
|
653
|
+
expect(capturedUrl).toContain("encoding=linear16");
|
|
654
|
+
expect(capturedUrl).toContain("container=wav");
|
|
655
|
+
expect(capturedUrl).not.toContain("sample_rate=");
|
|
656
|
+
expect(result.contentType).toBe("audio/wav");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("uses configured model", async () => {
|
|
660
|
+
mockDeepgramConfig.model = "aura-luna-en";
|
|
661
|
+
const audioPayload = new Uint8Array([0x49, 0x44, 0x33]);
|
|
662
|
+
let capturedUrl = "";
|
|
663
|
+
|
|
664
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
665
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
666
|
+
return new Response(audioPayload, { status: 200 });
|
|
667
|
+
}) as unknown as typeof globalThis.fetch;
|
|
668
|
+
|
|
669
|
+
const provider = createDeepgramProvider();
|
|
670
|
+
await provider.synthesize(makeRequest());
|
|
671
|
+
|
|
672
|
+
expect(capturedUrl).toContain("model=aura-luna-en");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// -- Content type / format -----------------------------------------------
|
|
676
|
+
|
|
677
|
+
test("returns audio/mpeg content type for mp3 format", async () => {
|
|
678
|
+
const audioPayload = new Uint8Array([0x49, 0x44, 0x33]);
|
|
679
|
+
mockFetchReturning(audioPayload);
|
|
680
|
+
|
|
681
|
+
const provider = createDeepgramProvider();
|
|
682
|
+
const result = await provider.synthesize(makeRequest());
|
|
683
|
+
|
|
684
|
+
expect(result.contentType).toBe("audio/mpeg");
|
|
685
|
+
expect(result.audio.byteLength).toBeGreaterThan(0);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// -- Required config validation ------------------------------------------
|
|
689
|
+
|
|
690
|
+
test("throws DEEPGRAM_TTS_NO_API_KEY when API key is missing", async () => {
|
|
691
|
+
mockDeepgramApiKey = null;
|
|
692
|
+
|
|
693
|
+
const provider = createDeepgramProvider();
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
await provider.synthesize(makeRequest());
|
|
697
|
+
throw new Error("Expected synthesize to throw");
|
|
698
|
+
} catch (err) {
|
|
699
|
+
expect(err).toBeInstanceOf(DeepgramTtsError);
|
|
700
|
+
expect((err as DeepgramTtsError).code).toBe("DEEPGRAM_TTS_NO_API_KEY");
|
|
701
|
+
expect((err as DeepgramTtsError).message).toContain(
|
|
702
|
+
"API key not configured",
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// -- Error handling ------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
test("throws DEEPGRAM_TTS_HTTP_ERROR on non-200 response", async () => {
|
|
710
|
+
mockFetchError(401, "Unauthorized");
|
|
711
|
+
|
|
712
|
+
const provider = createDeepgramProvider();
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
await provider.synthesize(makeRequest());
|
|
716
|
+
throw new Error("Expected synthesize to throw");
|
|
717
|
+
} catch (err) {
|
|
718
|
+
expect(err).toBeInstanceOf(DeepgramTtsError);
|
|
719
|
+
expect((err as DeepgramTtsError).code).toBe("DEEPGRAM_TTS_HTTP_ERROR");
|
|
720
|
+
expect((err as DeepgramTtsError).statusCode).toBe(401);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("throws DEEPGRAM_TTS_EMPTY_RESPONSE on empty audio body", async () => {
|
|
725
|
+
mockFetchReturning(new Uint8Array(0));
|
|
726
|
+
|
|
727
|
+
const provider = createDeepgramProvider();
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
await provider.synthesize(makeRequest());
|
|
731
|
+
throw new Error("Expected synthesize to throw");
|
|
732
|
+
} catch (err) {
|
|
733
|
+
expect(err).toBeInstanceOf(DeepgramTtsError);
|
|
734
|
+
expect((err as DeepgramTtsError).code).toBe(
|
|
735
|
+
"DEEPGRAM_TTS_EMPTY_RESPONSE",
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("throws DEEPGRAM_TTS_REQUEST_FAILED on network error", async () => {
|
|
741
|
+
globalThis.fetch = mock(async () => {
|
|
742
|
+
throw new Error("Network unreachable");
|
|
743
|
+
}) as unknown as typeof globalThis.fetch;
|
|
744
|
+
|
|
745
|
+
const provider = createDeepgramProvider();
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
await provider.synthesize(makeRequest());
|
|
749
|
+
throw new Error("Expected synthesize to throw");
|
|
750
|
+
} catch (err) {
|
|
751
|
+
expect(err).toBeInstanceOf(DeepgramTtsError);
|
|
752
|
+
expect((err as DeepgramTtsError).code).toBe(
|
|
753
|
+
"DEEPGRAM_TTS_REQUEST_FAILED",
|
|
754
|
+
);
|
|
755
|
+
expect((err as DeepgramTtsError).message).toContain(
|
|
756
|
+
"Network unreachable",
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// ===========================================================================
|
|
763
|
+
// Built-in registration
|
|
764
|
+
// ===========================================================================
|
|
765
|
+
|
|
766
|
+
describe("registerBuiltinTtsProviders", () => {
|
|
767
|
+
test("registers elevenlabs, fish-audio, and deepgram providers", () => {
|
|
768
|
+
registerBuiltinTtsProviders();
|
|
769
|
+
|
|
770
|
+
const providers = listTtsProviders();
|
|
771
|
+
const ids = providers.map((p) => p.id);
|
|
772
|
+
expect(ids).toContain("elevenlabs");
|
|
773
|
+
expect(ids).toContain("fish-audio");
|
|
774
|
+
expect(ids).toContain("deepgram");
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test("providers are discoverable via getTtsProvider after registration", () => {
|
|
778
|
+
registerBuiltinTtsProviders();
|
|
779
|
+
|
|
780
|
+
const el = getTtsProvider("elevenlabs");
|
|
781
|
+
expect(el.id).toBe("elevenlabs");
|
|
782
|
+
|
|
783
|
+
const fa = getTtsProvider("fish-audio");
|
|
784
|
+
expect(fa.id).toBe("fish-audio");
|
|
785
|
+
|
|
786
|
+
const dg = getTtsProvider("deepgram");
|
|
787
|
+
expect(dg.id).toBe("deepgram");
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test("idempotent — calling twice does not throw", () => {
|
|
791
|
+
// First call registers; second should be a no-op due to the guard flag.
|
|
792
|
+
// However, because tests reset the registry via afterEach, the internal
|
|
793
|
+
// `registered` flag may still be true. We call it once here and verify
|
|
794
|
+
// it does not throw — that exercises the guard path.
|
|
795
|
+
registerBuiltinTtsProviders();
|
|
796
|
+
expect(() => registerBuiltinTtsProviders()).not.toThrow();
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test("registers every provider declared in the catalog", () => {
|
|
800
|
+
registerBuiltinTtsProviders();
|
|
801
|
+
|
|
802
|
+
const catalogIds = listCatalogProviderIds();
|
|
803
|
+
const registeredProviders = listTtsProviders();
|
|
804
|
+
const registeredIds = registeredProviders.map((p) => p.id);
|
|
805
|
+
|
|
806
|
+
for (const id of catalogIds) {
|
|
807
|
+
expect(registeredIds).toContain(id);
|
|
808
|
+
}
|
|
809
|
+
// The registered set should match the catalog exactly (no extras).
|
|
810
|
+
expect(registeredIds.length).toBe(catalogIds.length);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test("every catalog provider has a factory in the providerFactories map", () => {
|
|
814
|
+
const catalogIds = listCatalogProviderIds();
|
|
815
|
+
|
|
816
|
+
for (const id of catalogIds) {
|
|
817
|
+
expect(providerFactories.has(id)).toBe(true);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("throws when a catalog provider has no adapter factory", () => {
|
|
822
|
+
// Verify the error path by checking that the error message format is
|
|
823
|
+
// correct. We cannot easily add a fake catalog entry without modifying
|
|
824
|
+
// the catalog module, but we can verify the factory map keys match the
|
|
825
|
+
// catalog — if they diverge this test will catch it.
|
|
826
|
+
const catalogIds = listCatalogProviderIds();
|
|
827
|
+
const factoryIds = [...providerFactories.keys()];
|
|
828
|
+
|
|
829
|
+
const missingFactories = catalogIds.filter(
|
|
830
|
+
(id) => !factoryIds.includes(id),
|
|
831
|
+
);
|
|
832
|
+
expect(missingFactories).toEqual([]);
|
|
833
|
+
});
|
|
834
|
+
});
|