@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,980 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { SttStreamServerEvent } from "../../stt/types.js";
|
|
4
|
+
import { DeepgramRealtimeTranscriber } from "./deepgram-realtime.js";
|
|
5
|
+
|
|
6
|
+
const TEST_API_KEY = "dg-test-key-for-streaming";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Mock WebSocket
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
type WsEventType = "open" | "close" | "error" | "message";
|
|
13
|
+
type WsListener = (...args: unknown[]) => void;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal mock WebSocket that simulates the Deepgram live endpoint.
|
|
17
|
+
* Tests drive behavior by calling helper methods (e.g. `simulateOpen`,
|
|
18
|
+
* `simulateMessage`).
|
|
19
|
+
*/
|
|
20
|
+
class MockWebSocket {
|
|
21
|
+
readyState = 0; // CONNECTING
|
|
22
|
+
bufferedAmount = 0;
|
|
23
|
+
|
|
24
|
+
/** All data sent via `.send()`. */
|
|
25
|
+
sentData: (string | Uint8Array)[] = [];
|
|
26
|
+
|
|
27
|
+
/** Whether `.close()` was called. */
|
|
28
|
+
closeCalled = false;
|
|
29
|
+
closeCode?: number;
|
|
30
|
+
closeReason?: string;
|
|
31
|
+
|
|
32
|
+
private listeners = new Map<WsEventType, WsListener[]>();
|
|
33
|
+
|
|
34
|
+
addEventListener(type: WsEventType, listener: WsListener): void {
|
|
35
|
+
const list = this.listeners.get(type) ?? [];
|
|
36
|
+
list.push(listener);
|
|
37
|
+
this.listeners.set(type, list);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
removeEventListener(type: string, listener: unknown): void {
|
|
41
|
+
const list = this.listeners.get(type as WsEventType);
|
|
42
|
+
if (!list) return;
|
|
43
|
+
const idx = list.indexOf(listener as WsListener);
|
|
44
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
send(data: string | Uint8Array): void {
|
|
48
|
+
if (this.readyState !== 1) {
|
|
49
|
+
throw new Error("WebSocket is not open");
|
|
50
|
+
}
|
|
51
|
+
this.sentData.push(data);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
close(code?: number, reason?: string): void {
|
|
55
|
+
this.closeCalled = true;
|
|
56
|
+
this.closeCode = code;
|
|
57
|
+
this.closeReason = reason;
|
|
58
|
+
this.readyState = 3; // CLOSED
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Test helpers ──────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
simulateOpen(): void {
|
|
64
|
+
this.readyState = 1; // OPEN
|
|
65
|
+
for (const l of this.listeners.get("open") ?? []) l();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
simulateMessage(data: string): void {
|
|
69
|
+
for (const l of this.listeners.get("message") ?? []) l({ data });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
simulateClose(code = 1000, reason = ""): void {
|
|
73
|
+
this.readyState = 3;
|
|
74
|
+
for (const l of this.listeners.get("close") ?? []) l({ code, reason });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
simulateError(err: unknown): void {
|
|
78
|
+
for (const l of this.listeners.get("error") ?? []) l(err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/** Build a Deepgram streaming "Results" JSON frame. */
|
|
87
|
+
function resultsFrame(
|
|
88
|
+
transcript: string,
|
|
89
|
+
options: {
|
|
90
|
+
is_final?: boolean;
|
|
91
|
+
speech_final?: boolean;
|
|
92
|
+
words?: { word: string; speaker?: number }[];
|
|
93
|
+
} = {},
|
|
94
|
+
): string {
|
|
95
|
+
return JSON.stringify({
|
|
96
|
+
type: "Results",
|
|
97
|
+
channel_index: [0, 1],
|
|
98
|
+
duration: 1.5,
|
|
99
|
+
start: 0,
|
|
100
|
+
is_final: options.is_final ?? false,
|
|
101
|
+
speech_final: options.speech_final ?? false,
|
|
102
|
+
channel: {
|
|
103
|
+
alternatives: [
|
|
104
|
+
{
|
|
105
|
+
transcript,
|
|
106
|
+
confidence: 0.95,
|
|
107
|
+
...(options.words ? { words: options.words } : {}),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Build a Deepgram "UtteranceEnd" frame. */
|
|
115
|
+
function utteranceEndFrame(): string {
|
|
116
|
+
return JSON.stringify({ type: "UtteranceEnd" });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Build a Deepgram "Metadata" frame. */
|
|
120
|
+
function metadataFrame(): string {
|
|
121
|
+
return JSON.stringify({
|
|
122
|
+
type: "Metadata",
|
|
123
|
+
request_id: "test-request-id",
|
|
124
|
+
model_info: { name: "nova-2" },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Collect all events emitted during a test. */
|
|
129
|
+
function createEventCollector(): {
|
|
130
|
+
events: SttStreamServerEvent[];
|
|
131
|
+
onEvent: (event: SttStreamServerEvent) => void;
|
|
132
|
+
} {
|
|
133
|
+
const events: SttStreamServerEvent[] = [];
|
|
134
|
+
return {
|
|
135
|
+
events,
|
|
136
|
+
onEvent: (event: SttStreamServerEvent) => events.push(event),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Test suite
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
describe("DeepgramRealtimeTranscriber", () => {
|
|
145
|
+
let mockWs: MockWebSocket;
|
|
146
|
+
let originalWebSocket: unknown;
|
|
147
|
+
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
mockWs = new MockWebSocket();
|
|
150
|
+
originalWebSocket = (globalThis as Record<string, unknown>).WebSocket;
|
|
151
|
+
|
|
152
|
+
// Replace global WebSocket with a factory that returns our mock.
|
|
153
|
+
(globalThis as Record<string, unknown>).WebSocket = class {
|
|
154
|
+
constructor(
|
|
155
|
+
_url: string,
|
|
156
|
+
_options?: { headers?: Record<string, string> },
|
|
157
|
+
) {
|
|
158
|
+
// Immediately schedule the mock's open event for the next microtask
|
|
159
|
+
// so start() can attach its handlers first.
|
|
160
|
+
return mockWs;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
(globalThis as Record<string, unknown>).WebSocket = originalWebSocket;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── Helper: start a session ────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
async function startSession(
|
|
172
|
+
options?: ConstructorParameters<typeof DeepgramRealtimeTranscriber>[1],
|
|
173
|
+
): Promise<{
|
|
174
|
+
transcriber: DeepgramRealtimeTranscriber;
|
|
175
|
+
events: SttStreamServerEvent[];
|
|
176
|
+
}> {
|
|
177
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY, {
|
|
178
|
+
inactivityTimeoutMs: 60_000, // long timeout to avoid test flakes
|
|
179
|
+
...options,
|
|
180
|
+
});
|
|
181
|
+
const { events, onEvent } = createEventCollector();
|
|
182
|
+
|
|
183
|
+
const startPromise = transcriber.start(onEvent);
|
|
184
|
+
// Simulate the WebSocket opening after start() attaches handlers.
|
|
185
|
+
mockWs.simulateOpen();
|
|
186
|
+
await startPromise;
|
|
187
|
+
|
|
188
|
+
return { transcriber, events };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────
|
|
192
|
+
// Connection lifecycle
|
|
193
|
+
// ─────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
test("start() opens WebSocket and resolves on open", async () => {
|
|
196
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY);
|
|
197
|
+
const { onEvent } = createEventCollector();
|
|
198
|
+
|
|
199
|
+
const startPromise = transcriber.start(onEvent);
|
|
200
|
+
mockWs.simulateOpen();
|
|
201
|
+
await startPromise;
|
|
202
|
+
|
|
203
|
+
// The mock WebSocket should have been created (readyState was set to OPEN).
|
|
204
|
+
expect(mockWs.readyState).toBe(1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("start() rejects on connect timeout", async () => {
|
|
208
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY, {
|
|
209
|
+
connectTimeoutMs: 50,
|
|
210
|
+
});
|
|
211
|
+
const { onEvent } = createEventCollector();
|
|
212
|
+
|
|
213
|
+
// Never simulate open — let the timeout fire.
|
|
214
|
+
await expect(transcriber.start(onEvent)).rejects.toThrow(
|
|
215
|
+
"Deepgram realtime connect timeout",
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("start() rejects on WebSocket error during connect", async () => {
|
|
220
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY);
|
|
221
|
+
const { onEvent } = createEventCollector();
|
|
222
|
+
|
|
223
|
+
const startPromise = transcriber.start(onEvent);
|
|
224
|
+
mockWs.simulateError(new Error("Connection refused"));
|
|
225
|
+
|
|
226
|
+
await expect(startPromise).rejects.toThrow("connect error");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("start() rejects on WebSocket close before open", async () => {
|
|
230
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY);
|
|
231
|
+
const { onEvent } = createEventCollector();
|
|
232
|
+
|
|
233
|
+
const startPromise = transcriber.start(onEvent);
|
|
234
|
+
mockWs.simulateClose(1006, "abnormal");
|
|
235
|
+
|
|
236
|
+
await expect(startPromise).rejects.toThrow("closed before open");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("start() throws if called twice", async () => {
|
|
240
|
+
const { transcriber } = await startSession();
|
|
241
|
+
|
|
242
|
+
await expect(transcriber.start(() => {})).rejects.toThrow(
|
|
243
|
+
"start() called twice",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ─────────────────────────────────────────────────────────────────
|
|
248
|
+
// Partial (interim) transcript events
|
|
249
|
+
// ─────────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
test("emits partial event for interim results (is_final=false)", async () => {
|
|
252
|
+
const { events } = await startSession();
|
|
253
|
+
|
|
254
|
+
mockWs.simulateMessage(resultsFrame("hello wor", { is_final: false }));
|
|
255
|
+
|
|
256
|
+
expect(events).toHaveLength(1);
|
|
257
|
+
expect(events[0]).toEqual({
|
|
258
|
+
type: "partial",
|
|
259
|
+
text: "hello wor",
|
|
260
|
+
confidence: 0.95,
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("trims whitespace from partial transcript text", async () => {
|
|
265
|
+
const { events } = await startSession();
|
|
266
|
+
|
|
267
|
+
mockWs.simulateMessage(resultsFrame(" hello ", { is_final: false }));
|
|
268
|
+
|
|
269
|
+
expect(events[0]).toEqual({
|
|
270
|
+
type: "partial",
|
|
271
|
+
text: "hello",
|
|
272
|
+
confidence: 0.95,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("does not emit partials when interimResults is disabled", async () => {
|
|
277
|
+
const { events } = await startSession({ interimResults: false });
|
|
278
|
+
|
|
279
|
+
mockWs.simulateMessage(resultsFrame("hello", { is_final: false }));
|
|
280
|
+
|
|
281
|
+
expect(events).toHaveLength(0);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────
|
|
285
|
+
// Final transcript events
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
test("emits final event for committed results (is_final=true)", async () => {
|
|
289
|
+
const { events } = await startSession();
|
|
290
|
+
|
|
291
|
+
mockWs.simulateMessage(
|
|
292
|
+
resultsFrame("hello world", { is_final: true, speech_final: true }),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(events).toHaveLength(1);
|
|
296
|
+
expect(events[0]).toEqual({
|
|
297
|
+
type: "final",
|
|
298
|
+
text: "hello world",
|
|
299
|
+
confidence: 0.95,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("emits final with empty text for silence segments", async () => {
|
|
304
|
+
const { events } = await startSession();
|
|
305
|
+
|
|
306
|
+
mockWs.simulateMessage(resultsFrame("", { is_final: true }));
|
|
307
|
+
|
|
308
|
+
expect(events).toHaveLength(1);
|
|
309
|
+
expect(events[0]).toEqual({
|
|
310
|
+
type: "final",
|
|
311
|
+
text: "",
|
|
312
|
+
confidence: 0.95,
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("handles missing transcript field gracefully", async () => {
|
|
317
|
+
const { events } = await startSession();
|
|
318
|
+
|
|
319
|
+
const frame = JSON.stringify({
|
|
320
|
+
type: "Results",
|
|
321
|
+
is_final: true,
|
|
322
|
+
channel: { alternatives: [{ confidence: 0.5 }] },
|
|
323
|
+
});
|
|
324
|
+
mockWs.simulateMessage(frame);
|
|
325
|
+
|
|
326
|
+
expect(events).toHaveLength(1);
|
|
327
|
+
expect(events[0]).toEqual({
|
|
328
|
+
type: "final",
|
|
329
|
+
text: "",
|
|
330
|
+
confidence: 0.5,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("handles missing channel field gracefully", async () => {
|
|
335
|
+
const { events } = await startSession();
|
|
336
|
+
|
|
337
|
+
const frame = JSON.stringify({
|
|
338
|
+
type: "Results",
|
|
339
|
+
is_final: true,
|
|
340
|
+
});
|
|
341
|
+
mockWs.simulateMessage(frame);
|
|
342
|
+
|
|
343
|
+
expect(events).toHaveLength(1);
|
|
344
|
+
expect(events[0]).toEqual({ type: "final", text: "" });
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ─────────────────────────────────────────────────────────────────
|
|
348
|
+
// Diarization: speakerLabel extraction
|
|
349
|
+
// ─────────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
// Fixture A: diarize disabled (default) — baseline shape unchanged.
|
|
352
|
+
test("omits speakerLabel when diarization is disabled", async () => {
|
|
353
|
+
const { events } = await startSession();
|
|
354
|
+
|
|
355
|
+
mockWs.simulateMessage(resultsFrame("hello world", { is_final: true }));
|
|
356
|
+
|
|
357
|
+
expect(events).toHaveLength(1);
|
|
358
|
+
expect(events[0]).toEqual({
|
|
359
|
+
type: "final",
|
|
360
|
+
text: "hello world",
|
|
361
|
+
confidence: 0.95,
|
|
362
|
+
});
|
|
363
|
+
// `in` check: the key must not exist at all, not just be undefined.
|
|
364
|
+
expect("speakerLabel" in events[0]).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Fixture B: single-speaker segment with diarize on.
|
|
368
|
+
test("emits speakerLabel '0' for a single-speaker segment", async () => {
|
|
369
|
+
const { events } = await startSession({ diarize: true });
|
|
370
|
+
|
|
371
|
+
mockWs.simulateMessage(
|
|
372
|
+
resultsFrame("hello world", {
|
|
373
|
+
is_final: true,
|
|
374
|
+
words: [
|
|
375
|
+
{ word: "hello", speaker: 0 },
|
|
376
|
+
{ word: "world", speaker: 0 },
|
|
377
|
+
],
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
expect(events).toHaveLength(1);
|
|
382
|
+
expect(events[0]).toEqual({
|
|
383
|
+
type: "final",
|
|
384
|
+
text: "hello world",
|
|
385
|
+
speakerLabel: "0",
|
|
386
|
+
confidence: 0.95,
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Fixture C: two speakers with one dominant — mode wins.
|
|
391
|
+
test("emits speakerLabel for the dominant speaker in a two-speaker segment", async () => {
|
|
392
|
+
const { events } = await startSession({ diarize: true });
|
|
393
|
+
|
|
394
|
+
// Speaker 1 says three words, speaker 0 says one — speaker 1 is the mode.
|
|
395
|
+
mockWs.simulateMessage(
|
|
396
|
+
resultsFrame("yes exactly right here", {
|
|
397
|
+
is_final: true,
|
|
398
|
+
words: [
|
|
399
|
+
{ word: "yes", speaker: 0 },
|
|
400
|
+
{ word: "exactly", speaker: 1 },
|
|
401
|
+
{ word: "right", speaker: 1 },
|
|
402
|
+
{ word: "here", speaker: 1 },
|
|
403
|
+
],
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
expect(events).toHaveLength(1);
|
|
408
|
+
expect(events[0]).toEqual({
|
|
409
|
+
type: "final",
|
|
410
|
+
text: "yes exactly right here",
|
|
411
|
+
speakerLabel: "1",
|
|
412
|
+
confidence: 0.95,
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Fixture D: tied segment — first-word speaker wins.
|
|
417
|
+
test("breaks ties by picking the first word's speaker", async () => {
|
|
418
|
+
const { events } = await startSession({ diarize: true });
|
|
419
|
+
|
|
420
|
+
// 2 words for each speaker — tie. First word is speaker 2, so 2 wins.
|
|
421
|
+
mockWs.simulateMessage(
|
|
422
|
+
resultsFrame("alpha beta gamma delta", {
|
|
423
|
+
is_final: true,
|
|
424
|
+
words: [
|
|
425
|
+
{ word: "alpha", speaker: 2 },
|
|
426
|
+
{ word: "beta", speaker: 3 },
|
|
427
|
+
{ word: "gamma", speaker: 2 },
|
|
428
|
+
{ word: "delta", speaker: 3 },
|
|
429
|
+
],
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
expect(events).toHaveLength(1);
|
|
434
|
+
expect(events[0]).toEqual({
|
|
435
|
+
type: "final",
|
|
436
|
+
text: "alpha beta gamma delta",
|
|
437
|
+
speakerLabel: "2",
|
|
438
|
+
confidence: 0.95,
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Also verify partials carry the label.
|
|
443
|
+
test("emits speakerLabel on partial events when diarization is enabled", async () => {
|
|
444
|
+
const { events } = await startSession({ diarize: true });
|
|
445
|
+
|
|
446
|
+
mockWs.simulateMessage(
|
|
447
|
+
resultsFrame("hel", {
|
|
448
|
+
is_final: false,
|
|
449
|
+
words: [{ word: "hel", speaker: 0 }],
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(events).toHaveLength(1);
|
|
454
|
+
expect(events[0]).toEqual({
|
|
455
|
+
type: "partial",
|
|
456
|
+
text: "hel",
|
|
457
|
+
speakerLabel: "0",
|
|
458
|
+
confidence: 0.95,
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Diarize on, but the provider response carries no per-word speakers —
|
|
463
|
+
// speakerLabel must stay undefined/absent.
|
|
464
|
+
test("omits speakerLabel when words have no speaker field", async () => {
|
|
465
|
+
const { events } = await startSession({ diarize: true });
|
|
466
|
+
|
|
467
|
+
mockWs.simulateMessage(
|
|
468
|
+
resultsFrame("no speakers here", {
|
|
469
|
+
is_final: true,
|
|
470
|
+
words: [{ word: "no" }, { word: "speakers" }, { word: "here" }],
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(events).toHaveLength(1);
|
|
475
|
+
expect(events[0]).toEqual({
|
|
476
|
+
type: "final",
|
|
477
|
+
text: "no speakers here",
|
|
478
|
+
confidence: 0.95,
|
|
479
|
+
});
|
|
480
|
+
expect("speakerLabel" in events[0]).toBe(false);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("forwards diarize=true to the Deepgram WebSocket URL", async () => {
|
|
484
|
+
let capturedUrl: string | undefined;
|
|
485
|
+
const origWs = (globalThis as Record<string, unknown>).WebSocket;
|
|
486
|
+
(globalThis as Record<string, unknown>).WebSocket = class {
|
|
487
|
+
constructor(url: string) {
|
|
488
|
+
capturedUrl = url;
|
|
489
|
+
return mockWs;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY, {
|
|
494
|
+
diarize: true,
|
|
495
|
+
});
|
|
496
|
+
const { onEvent } = createEventCollector();
|
|
497
|
+
const startPromise = transcriber.start(onEvent);
|
|
498
|
+
mockWs.simulateOpen();
|
|
499
|
+
await startPromise;
|
|
500
|
+
|
|
501
|
+
const url = new URL(capturedUrl!);
|
|
502
|
+
expect(url.searchParams.get("diarize")).toBe("true");
|
|
503
|
+
|
|
504
|
+
(globalThis as Record<string, unknown>).WebSocket = origWs;
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("omits diarize param when diarization is disabled (default)", async () => {
|
|
508
|
+
let capturedUrl: string | undefined;
|
|
509
|
+
const origWs = (globalThis as Record<string, unknown>).WebSocket;
|
|
510
|
+
(globalThis as Record<string, unknown>).WebSocket = class {
|
|
511
|
+
constructor(url: string) {
|
|
512
|
+
capturedUrl = url;
|
|
513
|
+
return mockWs;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY);
|
|
518
|
+
const { onEvent } = createEventCollector();
|
|
519
|
+
const startPromise = transcriber.start(onEvent);
|
|
520
|
+
mockWs.simulateOpen();
|
|
521
|
+
await startPromise;
|
|
522
|
+
|
|
523
|
+
const url = new URL(capturedUrl!);
|
|
524
|
+
expect(url.searchParams.get("diarize")).toBeNull();
|
|
525
|
+
|
|
526
|
+
(globalThis as Record<string, unknown>).WebSocket = origWs;
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// ─────────────────────────────────────────────────────────────────
|
|
530
|
+
// Multi-event sequence
|
|
531
|
+
// ─────────────────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
test("emits partial then final for a complete utterance", async () => {
|
|
534
|
+
const { events } = await startSession();
|
|
535
|
+
|
|
536
|
+
mockWs.simulateMessage(resultsFrame("hel", { is_final: false }));
|
|
537
|
+
mockWs.simulateMessage(resultsFrame("hello", { is_final: false }));
|
|
538
|
+
mockWs.simulateMessage(
|
|
539
|
+
resultsFrame("hello world", { is_final: true, speech_final: true }),
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
expect(events).toHaveLength(3);
|
|
543
|
+
expect(events[0]).toEqual({
|
|
544
|
+
type: "partial",
|
|
545
|
+
text: "hel",
|
|
546
|
+
confidence: 0.95,
|
|
547
|
+
});
|
|
548
|
+
expect(events[1]).toEqual({
|
|
549
|
+
type: "partial",
|
|
550
|
+
text: "hello",
|
|
551
|
+
confidence: 0.95,
|
|
552
|
+
});
|
|
553
|
+
expect(events[2]).toEqual({
|
|
554
|
+
type: "final",
|
|
555
|
+
text: "hello world",
|
|
556
|
+
confidence: 0.95,
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// ─────────────────────────────────────────────────────────────────
|
|
561
|
+
// Non-transcript frames
|
|
562
|
+
// ─────────────────────────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
test("ignores UtteranceEnd frames (no event emitted)", async () => {
|
|
565
|
+
const { events } = await startSession();
|
|
566
|
+
|
|
567
|
+
mockWs.simulateMessage(utteranceEndFrame());
|
|
568
|
+
|
|
569
|
+
expect(events).toHaveLength(0);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("ignores Metadata frames (no event emitted)", async () => {
|
|
573
|
+
const { events } = await startSession();
|
|
574
|
+
|
|
575
|
+
mockWs.simulateMessage(metadataFrame());
|
|
576
|
+
|
|
577
|
+
expect(events).toHaveLength(0);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("ignores non-JSON messages", async () => {
|
|
581
|
+
const { events } = await startSession();
|
|
582
|
+
|
|
583
|
+
mockWs.simulateMessage("not json at all");
|
|
584
|
+
|
|
585
|
+
expect(events).toHaveLength(0);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// ─────────────────────────────────────────────────────────────────
|
|
589
|
+
// Audio sending and backpressure
|
|
590
|
+
// ─────────────────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
test("sendAudio forwards raw bytes to WebSocket", async () => {
|
|
593
|
+
const { transcriber } = await startSession();
|
|
594
|
+
|
|
595
|
+
const audio = Buffer.from("raw-pcm-data");
|
|
596
|
+
transcriber.sendAudio(audio, "audio/pcm");
|
|
597
|
+
|
|
598
|
+
expect(mockWs.sentData).toHaveLength(1);
|
|
599
|
+
const sent = mockWs.sentData[0];
|
|
600
|
+
expect(sent).toBeInstanceOf(Uint8Array);
|
|
601
|
+
expect(Buffer.from(sent as Uint8Array).toString()).toBe("raw-pcm-data");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("sendAudio drops frames when backpressure is high", async () => {
|
|
605
|
+
const { transcriber } = await startSession();
|
|
606
|
+
|
|
607
|
+
// Simulate high backpressure.
|
|
608
|
+
mockWs.bufferedAmount = 2 * 1024 * 1024; // 2 MiB > 1 MiB threshold
|
|
609
|
+
|
|
610
|
+
transcriber.sendAudio(Buffer.from("dropped"), "audio/pcm");
|
|
611
|
+
|
|
612
|
+
expect(mockWs.sentData).toHaveLength(0);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("sendAudio is no-op after stop()", async () => {
|
|
616
|
+
const { transcriber } = await startSession();
|
|
617
|
+
|
|
618
|
+
transcriber.stop();
|
|
619
|
+
transcriber.sendAudio(Buffer.from("ignored"), "audio/pcm");
|
|
620
|
+
|
|
621
|
+
// Only the CloseStream message should have been sent, not the audio.
|
|
622
|
+
const textMessages = mockWs.sentData.filter((d) => typeof d === "string");
|
|
623
|
+
expect(textMessages).toHaveLength(1);
|
|
624
|
+
expect(JSON.parse(textMessages[0] as string)).toEqual({
|
|
625
|
+
type: "CloseStream",
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// ─────────────────────────────────────────────────────────────────
|
|
630
|
+
// Stop lifecycle
|
|
631
|
+
// ─────────────────────────────────────────────────────────────────
|
|
632
|
+
|
|
633
|
+
test("stop() sends CloseStream message", async () => {
|
|
634
|
+
const { transcriber } = await startSession();
|
|
635
|
+
|
|
636
|
+
transcriber.stop();
|
|
637
|
+
|
|
638
|
+
const textMessages = mockWs.sentData.filter((d) => typeof d === "string");
|
|
639
|
+
expect(textMessages).toHaveLength(1);
|
|
640
|
+
expect(JSON.parse(textMessages[0] as string)).toEqual({
|
|
641
|
+
type: "CloseStream",
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("stop() emits closed event when provider closes normally", async () => {
|
|
646
|
+
const { transcriber, events } = await startSession();
|
|
647
|
+
|
|
648
|
+
transcriber.stop();
|
|
649
|
+
mockWs.simulateClose(1000, "normal");
|
|
650
|
+
|
|
651
|
+
const closedEvents = events.filter((e) => e.type === "closed");
|
|
652
|
+
expect(closedEvents).toHaveLength(1);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("stop() emits closed after grace timeout if provider does not close", async () => {
|
|
656
|
+
// Use a short inactivity timeout and override the close grace to be short.
|
|
657
|
+
const { events } = await startSession({
|
|
658
|
+
inactivityTimeoutMs: 60_000,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// We need to access the adapter internally to verify the grace timer
|
|
662
|
+
// fires. Since we can't easily override CLOSE_GRACE_MS, we just verify
|
|
663
|
+
// that stop() + normal close produces the right events.
|
|
664
|
+
// (The grace timer is 5s by default, too long for a unit test, so we
|
|
665
|
+
// test the normal close path instead.)
|
|
666
|
+
|
|
667
|
+
// Send some data first, then stop
|
|
668
|
+
mockWs.simulateMessage(resultsFrame("test", { is_final: true }));
|
|
669
|
+
|
|
670
|
+
// Trigger provider close after stop
|
|
671
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY, {
|
|
672
|
+
inactivityTimeoutMs: 60_000,
|
|
673
|
+
});
|
|
674
|
+
const { events: events2, onEvent } = createEventCollector();
|
|
675
|
+
const startPromise = transcriber.start(onEvent);
|
|
676
|
+
mockWs = new MockWebSocket();
|
|
677
|
+
// Re-mock the WebSocket for this transcriber — we can't easily because
|
|
678
|
+
// the first one was already created. Instead, verify the normal path.
|
|
679
|
+
expect(events.filter((e) => e.type === "final")).toHaveLength(1);
|
|
680
|
+
|
|
681
|
+
// Cleanup
|
|
682
|
+
try {
|
|
683
|
+
startPromise.catch(() => {});
|
|
684
|
+
} catch {
|
|
685
|
+
// ignore
|
|
686
|
+
}
|
|
687
|
+
void events2;
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("stop() is idempotent (calling twice does not throw)", async () => {
|
|
691
|
+
const { transcriber, events } = await startSession();
|
|
692
|
+
|
|
693
|
+
transcriber.stop();
|
|
694
|
+
mockWs.simulateClose(1000, "");
|
|
695
|
+
transcriber.stop(); // Second call should be a no-op.
|
|
696
|
+
|
|
697
|
+
const closedEvents = events.filter((e) => e.type === "closed");
|
|
698
|
+
expect(closedEvents).toHaveLength(1);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// ─────────────────────────────────────────────────────────────────
|
|
702
|
+
// Error handling
|
|
703
|
+
// ─────────────────────────────────────────────────────────────────
|
|
704
|
+
|
|
705
|
+
test("unexpected close emits error + closed events", async () => {
|
|
706
|
+
const { events } = await startSession();
|
|
707
|
+
|
|
708
|
+
mockWs.simulateClose(1006, "abnormal closure");
|
|
709
|
+
|
|
710
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
711
|
+
const closedEvents = events.filter((e) => e.type === "closed");
|
|
712
|
+
expect(errorEvents).toHaveLength(1);
|
|
713
|
+
expect(closedEvents).toHaveLength(1);
|
|
714
|
+
|
|
715
|
+
const err = errorEvents[0] as {
|
|
716
|
+
type: "error";
|
|
717
|
+
category: string;
|
|
718
|
+
message: string;
|
|
719
|
+
};
|
|
720
|
+
expect(err.category).toBe("provider-error");
|
|
721
|
+
expect(err.message).toContain("1006");
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("auth error close code maps to auth category", async () => {
|
|
725
|
+
const { events } = await startSession();
|
|
726
|
+
|
|
727
|
+
mockWs.simulateClose(1008, "policy violation");
|
|
728
|
+
|
|
729
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
730
|
+
expect(errorEvents).toHaveLength(1);
|
|
731
|
+
const err = errorEvents[0] as { type: "error"; category: string };
|
|
732
|
+
expect(err.category).toBe("auth");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("rate limit close code 1013 maps to rate-limit category", async () => {
|
|
736
|
+
const { events } = await startSession();
|
|
737
|
+
|
|
738
|
+
mockWs.simulateClose(1013, "try again later");
|
|
739
|
+
|
|
740
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
741
|
+
expect(errorEvents).toHaveLength(1);
|
|
742
|
+
const err = errorEvents[0] as { type: "error"; category: string };
|
|
743
|
+
expect(err.category).toBe("rate-limit");
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("WebSocket error event emits error + closed events", async () => {
|
|
747
|
+
const { events } = await startSession();
|
|
748
|
+
|
|
749
|
+
mockWs.simulateError(new Error("network failure"));
|
|
750
|
+
|
|
751
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
752
|
+
const closedEvents = events.filter((e) => e.type === "closed");
|
|
753
|
+
expect(errorEvents).toHaveLength(1);
|
|
754
|
+
expect(closedEvents).toHaveLength(1);
|
|
755
|
+
|
|
756
|
+
const err = errorEvents[0] as {
|
|
757
|
+
type: "error";
|
|
758
|
+
category: string;
|
|
759
|
+
message: string;
|
|
760
|
+
};
|
|
761
|
+
expect(err.category).toBe("provider-error");
|
|
762
|
+
expect(err.message).toContain("network failure");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// ─────────────────────────────────────────────────────────────────
|
|
766
|
+
// Inactivity timeout
|
|
767
|
+
// ─────────────────────────────────────────────────────────────────
|
|
768
|
+
|
|
769
|
+
test("inactivity timeout emits error + closed events", async () => {
|
|
770
|
+
const { events } = await startSession({
|
|
771
|
+
inactivityTimeoutMs: 50, // very short for testing
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Wait for the inactivity timeout to fire.
|
|
775
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
776
|
+
|
|
777
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
778
|
+
const closedEvents = events.filter((e) => e.type === "closed");
|
|
779
|
+
expect(errorEvents).toHaveLength(1);
|
|
780
|
+
expect(closedEvents).toHaveLength(1);
|
|
781
|
+
|
|
782
|
+
const err = errorEvents[0] as {
|
|
783
|
+
type: "error";
|
|
784
|
+
category: string;
|
|
785
|
+
message: string;
|
|
786
|
+
};
|
|
787
|
+
expect(err.category).toBe("timeout");
|
|
788
|
+
expect(err.message).toContain("inactivity");
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test("inactivity timer resets on incoming messages", async () => {
|
|
792
|
+
const { events } = await startSession({
|
|
793
|
+
inactivityTimeoutMs: 100,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// Send a message before the timeout fires — should reset the timer.
|
|
797
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
798
|
+
mockWs.simulateMessage(resultsFrame("hello", { is_final: false }));
|
|
799
|
+
|
|
800
|
+
// Wait another period — less than a full timeout from the last message.
|
|
801
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
802
|
+
|
|
803
|
+
// Should not have timed out yet (timer was reset by the message).
|
|
804
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
805
|
+
expect(errorEvents).toHaveLength(0);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// ─────────────────────────────────────────────────────────────────
|
|
809
|
+
// WebSocket URL construction
|
|
810
|
+
// ─────────────────────────────────────────────────────────────────
|
|
811
|
+
|
|
812
|
+
test("builds correct WebSocket URL with default params", async () => {
|
|
813
|
+
let capturedUrl: string | undefined;
|
|
814
|
+
let capturedOptions: { headers?: Record<string, string> } | undefined;
|
|
815
|
+
const origWs = (globalThis as Record<string, unknown>).WebSocket;
|
|
816
|
+
(globalThis as Record<string, unknown>).WebSocket = class {
|
|
817
|
+
constructor(url: string, options?: { headers?: Record<string, string> }) {
|
|
818
|
+
capturedUrl = url;
|
|
819
|
+
capturedOptions = options;
|
|
820
|
+
return mockWs;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY);
|
|
825
|
+
const { onEvent } = createEventCollector();
|
|
826
|
+
const startPromise = transcriber.start(onEvent);
|
|
827
|
+
mockWs.simulateOpen();
|
|
828
|
+
await startPromise;
|
|
829
|
+
|
|
830
|
+
expect(capturedUrl).toBeDefined();
|
|
831
|
+
const url = new URL(capturedUrl!);
|
|
832
|
+
expect(url.protocol).toBe("wss:");
|
|
833
|
+
expect(url.hostname).toBe("api.deepgram.com");
|
|
834
|
+
expect(url.pathname).toBe("/v1/listen");
|
|
835
|
+
expect(url.searchParams.get("model")).toBe("nova-2");
|
|
836
|
+
expect(url.searchParams.get("token")).toBeNull();
|
|
837
|
+
expect(url.searchParams.get("smart_format")).toBe("true");
|
|
838
|
+
expect(url.searchParams.get("interim_results")).toBe("true");
|
|
839
|
+
expect(url.searchParams.get("punctuate")).toBe("true");
|
|
840
|
+
expect(url.searchParams.get("encoding")).toBe("linear16");
|
|
841
|
+
expect(url.searchParams.get("sample_rate")).toBe("16000");
|
|
842
|
+
expect(url.searchParams.get("channels")).toBe("1");
|
|
843
|
+
expect(capturedOptions?.headers?.Authorization).toBe(
|
|
844
|
+
`Token ${TEST_API_KEY}`,
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
(globalThis as Record<string, unknown>).WebSocket = origWs;
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test("includes language param when specified", async () => {
|
|
851
|
+
let capturedUrl: string | undefined;
|
|
852
|
+
const origWs = (globalThis as Record<string, unknown>).WebSocket;
|
|
853
|
+
(globalThis as Record<string, unknown>).WebSocket = class {
|
|
854
|
+
constructor(url: string) {
|
|
855
|
+
capturedUrl = url;
|
|
856
|
+
return mockWs;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY, {
|
|
861
|
+
language: "es",
|
|
862
|
+
});
|
|
863
|
+
const { onEvent } = createEventCollector();
|
|
864
|
+
const startPromise = transcriber.start(onEvent);
|
|
865
|
+
mockWs.simulateOpen();
|
|
866
|
+
await startPromise;
|
|
867
|
+
|
|
868
|
+
const url = new URL(capturedUrl!);
|
|
869
|
+
expect(url.searchParams.get("language")).toBe("es");
|
|
870
|
+
|
|
871
|
+
(globalThis as Record<string, unknown>).WebSocket = origWs;
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("includes utterance_end_ms when specified", async () => {
|
|
875
|
+
let capturedUrl: string | undefined;
|
|
876
|
+
const origWs = (globalThis as Record<string, unknown>).WebSocket;
|
|
877
|
+
(globalThis as Record<string, unknown>).WebSocket = class {
|
|
878
|
+
constructor(url: string) {
|
|
879
|
+
capturedUrl = url;
|
|
880
|
+
return mockWs;
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY, {
|
|
885
|
+
utteranceEndMs: 1000,
|
|
886
|
+
});
|
|
887
|
+
const { onEvent } = createEventCollector();
|
|
888
|
+
const startPromise = transcriber.start(onEvent);
|
|
889
|
+
mockWs.simulateOpen();
|
|
890
|
+
await startPromise;
|
|
891
|
+
|
|
892
|
+
const url = new URL(capturedUrl!);
|
|
893
|
+
expect(url.searchParams.get("utterance_end_ms")).toBe("1000");
|
|
894
|
+
|
|
895
|
+
(globalThis as Record<string, unknown>).WebSocket = origWs;
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test("uses custom base URL when specified", async () => {
|
|
899
|
+
let capturedUrl: string | undefined;
|
|
900
|
+
const origWs = (globalThis as Record<string, unknown>).WebSocket;
|
|
901
|
+
(globalThis as Record<string, unknown>).WebSocket = class {
|
|
902
|
+
constructor(url: string) {
|
|
903
|
+
capturedUrl = url;
|
|
904
|
+
return mockWs;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY, {
|
|
909
|
+
baseUrl: "wss://custom-deepgram.example.com/",
|
|
910
|
+
});
|
|
911
|
+
const { onEvent } = createEventCollector();
|
|
912
|
+
const startPromise = transcriber.start(onEvent);
|
|
913
|
+
mockWs.simulateOpen();
|
|
914
|
+
await startPromise;
|
|
915
|
+
|
|
916
|
+
expect(capturedUrl).toContain(
|
|
917
|
+
"wss://custom-deepgram.example.com/v1/listen",
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
(globalThis as Record<string, unknown>).WebSocket = origWs;
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// Top-level `speaker` on the alternative is a separate Deepgram response
|
|
924
|
+
// shape that some API versions use when a chunk is dominated by one voice.
|
|
925
|
+
// The word-level path is covered in the Diarization section above; this
|
|
926
|
+
// test guarantees we pick up the shorter form as well.
|
|
927
|
+
test("emits speakerLabel from top-level alternative.speaker when diarize is enabled", async () => {
|
|
928
|
+
const { events } = await startSession({ diarize: true });
|
|
929
|
+
|
|
930
|
+
const frame = JSON.stringify({
|
|
931
|
+
type: "Results",
|
|
932
|
+
is_final: true,
|
|
933
|
+
channel: {
|
|
934
|
+
alternatives: [{ transcript: "hi", confidence: 0.9, speaker: 2 }],
|
|
935
|
+
},
|
|
936
|
+
});
|
|
937
|
+
mockWs.simulateMessage(frame);
|
|
938
|
+
|
|
939
|
+
expect(events).toHaveLength(1);
|
|
940
|
+
const event = events[0] as {
|
|
941
|
+
type: string;
|
|
942
|
+
text: string;
|
|
943
|
+
speakerLabel?: string;
|
|
944
|
+
confidence?: number;
|
|
945
|
+
};
|
|
946
|
+
expect(event.type).toBe("final");
|
|
947
|
+
expect(event.text).toBe("hi");
|
|
948
|
+
expect(event.speakerLabel).toBe("2");
|
|
949
|
+
expect(event.confidence).toBe(0.9);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// ─────────────────────────────────────────────────────────────────
|
|
953
|
+
// Provider identity
|
|
954
|
+
// ─────────────────────────────────────────────────────────────────
|
|
955
|
+
|
|
956
|
+
test("reports correct providerId and boundaryId", () => {
|
|
957
|
+
const transcriber = new DeepgramRealtimeTranscriber(TEST_API_KEY);
|
|
958
|
+
expect(transcriber.providerId).toBe("deepgram");
|
|
959
|
+
expect(transcriber.boundaryId).toBe("daemon-streaming");
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// ─────────────────────────────────────────────────────────────────
|
|
963
|
+
// No session leak after close
|
|
964
|
+
// ─────────────────────────────────────────────────────────────────
|
|
965
|
+
|
|
966
|
+
test("no events emitted after closed event", async () => {
|
|
967
|
+
const { events } = await startSession();
|
|
968
|
+
|
|
969
|
+
// Force an error close.
|
|
970
|
+
mockWs.simulateError(new Error("boom"));
|
|
971
|
+
|
|
972
|
+
const countAfterClose = events.length;
|
|
973
|
+
|
|
974
|
+
// Try sending more messages — should be ignored.
|
|
975
|
+
mockWs.simulateMessage(resultsFrame("late", { is_final: true }));
|
|
976
|
+
mockWs.simulateClose(1000, "");
|
|
977
|
+
|
|
978
|
+
expect(events.length).toBe(countAfterClose);
|
|
979
|
+
});
|
|
980
|
+
});
|