@vellumai/assistant 0.11.3 → 0.11.4-staging.2
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 +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +139 -37
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +177 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +7 -3
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +19 -3
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +83 -12
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { setConfig } from "../../../__tests__/helpers/set-config.js";
|
|
4
|
+
import type {
|
|
5
|
+
StreamingTranscriber,
|
|
6
|
+
SttStreamServerEvent,
|
|
7
|
+
} from "../../../stt/types.js";
|
|
8
|
+
import { SttError } from "../../../stt/types.js";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Logger mock: must be declared before the subject import
|
|
12
|
+
//
|
|
13
|
+
// The adapter's only observable output for the chunk-cadence measurement is a
|
|
14
|
+
// log line, so it is asserted through captured logs.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
interface CapturedLog {
|
|
18
|
+
data: unknown;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const infoLogs: CapturedLog[] = [];
|
|
23
|
+
|
|
24
|
+
function capture(sink: CapturedLog[]) {
|
|
25
|
+
return (data: unknown, message?: string) => {
|
|
26
|
+
sink.push(
|
|
27
|
+
typeof data === "string"
|
|
28
|
+
? { data: undefined, message: data }
|
|
29
|
+
: { data, message: message ?? "" },
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
mock.module("../../../util/logger.js", () => {
|
|
35
|
+
const logger = {
|
|
36
|
+
info: capture(infoLogs),
|
|
37
|
+
debug: () => {},
|
|
38
|
+
warn: () => {},
|
|
39
|
+
error: () => {},
|
|
40
|
+
trace: () => {},
|
|
41
|
+
fatal: () => {},
|
|
42
|
+
child: () => logger,
|
|
43
|
+
};
|
|
44
|
+
return { getLogger: () => logger };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Mock WebSocket
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
type WsEventType = "open" | "close" | "error" | "message";
|
|
52
|
+
type WsListener = (...args: unknown[]) => void;
|
|
53
|
+
|
|
54
|
+
/** Minimal mock WebSocket standing in for Deepgram's `/v2/listen` endpoint. */
|
|
55
|
+
class MockWebSocket {
|
|
56
|
+
readyState = 0; // CONNECTING
|
|
57
|
+
bufferedAmount = 0;
|
|
58
|
+
|
|
59
|
+
/** Everything passed to `.send()`, in order. */
|
|
60
|
+
sentData: (string | Uint8Array)[] = [];
|
|
61
|
+
|
|
62
|
+
private listeners = new Map<WsEventType, WsListener[]>();
|
|
63
|
+
|
|
64
|
+
addEventListener(type: WsEventType, listener: WsListener): void {
|
|
65
|
+
const list = this.listeners.get(type) ?? [];
|
|
66
|
+
list.push(listener);
|
|
67
|
+
this.listeners.set(type, list);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
removeEventListener(type: string, listener: unknown): void {
|
|
71
|
+
const list = this.listeners.get(type as WsEventType);
|
|
72
|
+
const idx = list?.indexOf(listener as WsListener) ?? -1;
|
|
73
|
+
if (list && idx !== -1) {
|
|
74
|
+
list.splice(idx, 1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
send(data: string | Uint8Array): void {
|
|
79
|
+
if (this.readyState !== 1) {
|
|
80
|
+
throw new Error("WebSocket is not open");
|
|
81
|
+
}
|
|
82
|
+
this.sentData.push(data);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
close(): void {
|
|
86
|
+
this.readyState = 3; // CLOSED
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Test helpers ──────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
simulateOpen(): void {
|
|
92
|
+
this.readyState = 1; // OPEN
|
|
93
|
+
for (const l of this.listeners.get("open") ?? []) {
|
|
94
|
+
l();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
simulateMessage(data: string): void {
|
|
99
|
+
for (const l of this.listeners.get("message") ?? []) {
|
|
100
|
+
l({ data });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
simulateClose(code = 1000, reason = ""): void {
|
|
105
|
+
this.readyState = 3;
|
|
106
|
+
for (const l of this.listeners.get("close") ?? []) {
|
|
107
|
+
l({ code, reason });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
simulateError(err: unknown): void {
|
|
112
|
+
for (const l of this.listeners.get("error") ?? []) {
|
|
113
|
+
l(err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Subject import (after mocks)
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const { DeepgramFluxRealtimeTranscriber } =
|
|
123
|
+
await import("../deepgram-flux-realtime.js");
|
|
124
|
+
|
|
125
|
+
const TEST_API_KEY = "dg-flux-test-key";
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Helpers
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/** Build a Flux `TurnInfo` frame carrying the given turn state. */
|
|
132
|
+
function turnInfoFrame(
|
|
133
|
+
event: string,
|
|
134
|
+
extra: Record<string, unknown> = {},
|
|
135
|
+
): string {
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
type: "TurnInfo",
|
|
138
|
+
event,
|
|
139
|
+
request_id: "flux-request-id",
|
|
140
|
+
turn_index: 0,
|
|
141
|
+
audio_window_start: 0,
|
|
142
|
+
audio_window_end: 1.5,
|
|
143
|
+
...extra,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe("DeepgramFluxRealtimeTranscriber", () => {
|
|
148
|
+
let mockWs: MockWebSocket;
|
|
149
|
+
let dialedUrls: string[];
|
|
150
|
+
let dialedOptions: ({ headers?: Record<string, string> } | undefined)[];
|
|
151
|
+
let originalWebSocket: unknown;
|
|
152
|
+
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
// A fully default config: the acceptance criteria are about what the
|
|
155
|
+
// shipped defaults dial.
|
|
156
|
+
setConfig("liveVoice", {});
|
|
157
|
+
|
|
158
|
+
mockWs = new MockWebSocket();
|
|
159
|
+
dialedUrls = [];
|
|
160
|
+
dialedOptions = [];
|
|
161
|
+
infoLogs.length = 0;
|
|
162
|
+
|
|
163
|
+
originalWebSocket = (globalThis as Record<string, unknown>).WebSocket;
|
|
164
|
+
(globalThis as Record<string, unknown>).WebSocket = class {
|
|
165
|
+
constructor(url: string, options?: { headers?: Record<string, string> }) {
|
|
166
|
+
dialedUrls.push(url);
|
|
167
|
+
dialedOptions.push(options);
|
|
168
|
+
return mockWs;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterEach(() => {
|
|
174
|
+
(globalThis as Record<string, unknown>).WebSocket = originalWebSocket;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
async function startSession(
|
|
178
|
+
options: ConstructorParameters<
|
|
179
|
+
typeof DeepgramFluxRealtimeTranscriber
|
|
180
|
+
>[1] = {},
|
|
181
|
+
): Promise<{
|
|
182
|
+
transcriber: InstanceType<typeof DeepgramFluxRealtimeTranscriber>;
|
|
183
|
+
events: SttStreamServerEvent[];
|
|
184
|
+
}> {
|
|
185
|
+
const transcriber = new DeepgramFluxRealtimeTranscriber(TEST_API_KEY, {
|
|
186
|
+
// Long enough that no watchdog fires mid-test.
|
|
187
|
+
inactivityTimeoutMs: 60_000,
|
|
188
|
+
keepaliveIntervalMs: 0,
|
|
189
|
+
...options,
|
|
190
|
+
});
|
|
191
|
+
const events: SttStreamServerEvent[] = [];
|
|
192
|
+
|
|
193
|
+
const startPromise = transcriber.start((event) => events.push(event));
|
|
194
|
+
mockWs.simulateOpen();
|
|
195
|
+
await startPromise;
|
|
196
|
+
|
|
197
|
+
return { transcriber, events };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────
|
|
201
|
+
// Dialed URL
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
describe("session URL", () => {
|
|
205
|
+
test("dials /v2/listen with the default Flux tuning and no eager threshold", async () => {
|
|
206
|
+
await startSession();
|
|
207
|
+
|
|
208
|
+
expect(dialedUrls).toHaveLength(1);
|
|
209
|
+
const url = new URL(dialedUrls[0]!);
|
|
210
|
+
|
|
211
|
+
expect(url.protocol).toBe("wss:");
|
|
212
|
+
expect(url.host).toBe("api.deepgram.com");
|
|
213
|
+
expect(url.pathname).toBe("/v2/listen");
|
|
214
|
+
expect(url.searchParams.get("model")).toBe("flux-general-en");
|
|
215
|
+
expect(url.searchParams.get("eot_threshold")).toBe("0.7");
|
|
216
|
+
expect(url.searchParams.get("eot_timeout_ms")).toBe("5000");
|
|
217
|
+
// Unset eager threshold is what keeps Deepgram from speculating turns.
|
|
218
|
+
expect(url.searchParams.has("eager_eot_threshold")).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("sends the negotiated raw-audio format", async () => {
|
|
222
|
+
await startSession({ sampleRate: 24_000 });
|
|
223
|
+
|
|
224
|
+
const url = new URL(dialedUrls[0]!);
|
|
225
|
+
expect(url.searchParams.get("encoding")).toBe("linear16");
|
|
226
|
+
expect(url.searchParams.get("sample_rate")).toBe("24000");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("authenticates with the Token header, never the query string", async () => {
|
|
230
|
+
await startSession();
|
|
231
|
+
|
|
232
|
+
expect(dialedOptions[0]?.headers?.Authorization).toBe(
|
|
233
|
+
`Token ${TEST_API_KEY}`,
|
|
234
|
+
);
|
|
235
|
+
expect(dialedUrls[0]).not.toContain(TEST_API_KEY);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("takes its tuning from liveVoice.flux", async () => {
|
|
239
|
+
setConfig("liveVoice", {
|
|
240
|
+
flux: {
|
|
241
|
+
model: "flux-general-multi",
|
|
242
|
+
eotThreshold: 0.9,
|
|
243
|
+
eagerEotThreshold: 0.4,
|
|
244
|
+
eotTimeoutMs: 1_500,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await startSession();
|
|
249
|
+
|
|
250
|
+
const url = new URL(dialedUrls[0]!);
|
|
251
|
+
expect(url.searchParams.get("model")).toBe("flux-general-multi");
|
|
252
|
+
expect(url.searchParams.get("eot_threshold")).toBe("0.9");
|
|
253
|
+
expect(url.searchParams.get("eager_eot_threshold")).toBe("0.4");
|
|
254
|
+
expect(url.searchParams.get("eot_timeout_ms")).toBe("1500");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─────────────────────────────────────────────────────────────────
|
|
259
|
+
// Audio forwarding
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
describe("audio forwarding", () => {
|
|
263
|
+
test("forwards audio chunks to the socket as raw bytes", async () => {
|
|
264
|
+
const { transcriber } = await startSession();
|
|
265
|
+
|
|
266
|
+
transcriber.sendAudio(Buffer.from([1, 2, 3, 4]), "audio/pcm");
|
|
267
|
+
transcriber.sendAudio(Buffer.from([5, 6]), "audio/pcm");
|
|
268
|
+
|
|
269
|
+
expect(mockWs.sentData).toHaveLength(2);
|
|
270
|
+
expect(Array.from(mockWs.sentData[0] as Uint8Array)).toEqual([
|
|
271
|
+
1, 2, 3, 4,
|
|
272
|
+
]);
|
|
273
|
+
expect(Array.from(mockWs.sentData[1] as Uint8Array)).toEqual([5, 6]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("drops audio once the outbound buffer is saturated", async () => {
|
|
277
|
+
const { transcriber } = await startSession();
|
|
278
|
+
mockWs.bufferedAmount = 8 * 1024 * 1024;
|
|
279
|
+
|
|
280
|
+
transcriber.sendAudio(Buffer.from([1, 2]), "audio/pcm");
|
|
281
|
+
|
|
282
|
+
expect(mockWs.sentData).toHaveLength(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("logs the observed chunk duration once per session at info", async () => {
|
|
286
|
+
const { transcriber } = await startSession({ sampleRate: 16_000 });
|
|
287
|
+
|
|
288
|
+
// 2560 bytes of mono linear16 at 16kHz is exactly 80ms.
|
|
289
|
+
transcriber.sendAudio(Buffer.alloc(2560), "audio/pcm");
|
|
290
|
+
transcriber.sendAudio(Buffer.alloc(2560), "audio/pcm");
|
|
291
|
+
|
|
292
|
+
// The runbook reads this line off a default daemon, whose pino level is
|
|
293
|
+
// `info`: at debug it would never reach the log it is read from.
|
|
294
|
+
const cadenceLogs = infoLogs.filter((entry) =>
|
|
295
|
+
entry.message.includes("chunk cadence"),
|
|
296
|
+
);
|
|
297
|
+
expect(cadenceLogs).toHaveLength(1);
|
|
298
|
+
expect(cadenceLogs[0]!.data).toMatchObject({
|
|
299
|
+
observedChunkMs: 80,
|
|
300
|
+
recommendedChunkMs: 80,
|
|
301
|
+
sampleRate: 16_000,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ─────────────────────────────────────────────────────────────────
|
|
307
|
+
// Frame handling
|
|
308
|
+
// ─────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
describe("frame handling", () => {
|
|
311
|
+
test("a scripted turn produces the expected event stream", async () => {
|
|
312
|
+
const { events } = await startSession();
|
|
313
|
+
|
|
314
|
+
mockWs.simulateMessage(
|
|
315
|
+
JSON.stringify({ type: "Connected", request_id: "flux-request-id" }),
|
|
316
|
+
);
|
|
317
|
+
mockWs.simulateMessage(turnInfoFrame("StartOfTurn"));
|
|
318
|
+
mockWs.simulateMessage(
|
|
319
|
+
turnInfoFrame("Update", { transcript: "what is the" }),
|
|
320
|
+
);
|
|
321
|
+
mockWs.simulateMessage(
|
|
322
|
+
turnInfoFrame("EndOfTurn", {
|
|
323
|
+
transcript: "what is the weather",
|
|
324
|
+
end_of_turn_confidence: 0.91,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(events).toEqual([
|
|
329
|
+
{ type: "turn-start", turnIndex: 0 },
|
|
330
|
+
{ type: "partial", text: "what is the" },
|
|
331
|
+
// The final comes first so a consumer that ignores turn events still
|
|
332
|
+
// commits the transcript exactly as it does for any other provider.
|
|
333
|
+
{ type: "final", text: "what is the weather" },
|
|
334
|
+
{
|
|
335
|
+
type: "turn-end",
|
|
336
|
+
text: "what is the weather",
|
|
337
|
+
confidence: 0.91,
|
|
338
|
+
turnIndex: 0,
|
|
339
|
+
},
|
|
340
|
+
]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("an EndOfTurn frame emits final before turn-end", async () => {
|
|
344
|
+
const { events } = await startSession();
|
|
345
|
+
|
|
346
|
+
mockWs.simulateMessage(
|
|
347
|
+
turnInfoFrame("EndOfTurn", { transcript: "done" }),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(events.map((event) => event.type)).toEqual(["final", "turn-end"]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("a fatal Error frame surfaces its category", async () => {
|
|
354
|
+
const { events } = await startSession();
|
|
355
|
+
|
|
356
|
+
mockWs.simulateMessage(
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
type: "Error",
|
|
359
|
+
code: "INVALID_AUTH",
|
|
360
|
+
description: "Invalid credentials",
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
expect(events).toHaveLength(1);
|
|
365
|
+
expect(events[0]).toMatchObject({ type: "error", category: "auth" });
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("unrecognized and malformed frames leave the session transcribing", async () => {
|
|
369
|
+
const { events } = await startSession();
|
|
370
|
+
|
|
371
|
+
// Deepgram can add frame types without warning, so anything the parser
|
|
372
|
+
// does not recognize is dropped rather than raised as a stream failure.
|
|
373
|
+
mockWs.simulateMessage("not json at all");
|
|
374
|
+
mockWs.simulateMessage(JSON.stringify([1, 2, 3]));
|
|
375
|
+
mockWs.simulateMessage(
|
|
376
|
+
JSON.stringify({ type: "SomethingNewFromDeepgram" }),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(events).toEqual([]);
|
|
380
|
+
|
|
381
|
+
mockWs.simulateMessage(
|
|
382
|
+
turnInfoFrame("EndOfTurn", { transcript: "still here" }),
|
|
383
|
+
);
|
|
384
|
+
expect(events).toEqual([
|
|
385
|
+
{ type: "final", text: "still here" },
|
|
386
|
+
{ type: "turn-end", text: "still here", turnIndex: 0 },
|
|
387
|
+
]);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ─────────────────────────────────────────────────────────────────
|
|
392
|
+
// Error mapping
|
|
393
|
+
// ─────────────────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
describe("error mapping", () => {
|
|
396
|
+
test("an auth rejection during connect rejects with an auth SttError", async () => {
|
|
397
|
+
const transcriber = new DeepgramFluxRealtimeTranscriber(TEST_API_KEY);
|
|
398
|
+
const startPromise = transcriber.start(() => {});
|
|
399
|
+
mockWs.simulateClose(1008, "invalid credentials");
|
|
400
|
+
|
|
401
|
+
const err = await startPromise.catch((e: unknown) => e);
|
|
402
|
+
expect(err).toBeInstanceOf(SttError);
|
|
403
|
+
expect((err as SttError).category).toBe("auth");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("a connect timeout rejects with a timeout SttError", async () => {
|
|
407
|
+
const transcriber = new DeepgramFluxRealtimeTranscriber(TEST_API_KEY, {
|
|
408
|
+
connectTimeoutMs: 20,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const err = await transcriber.start(() => {}).catch((e: unknown) => e);
|
|
412
|
+
expect(err).toBeInstanceOf(SttError);
|
|
413
|
+
expect((err as SttError).category).toBe("timeout");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("an in-session auth close emits an auth error then closed", async () => {
|
|
417
|
+
const { events } = await startSession();
|
|
418
|
+
|
|
419
|
+
mockWs.simulateClose(1008, "invalid credentials");
|
|
420
|
+
|
|
421
|
+
expect(events).toHaveLength(2);
|
|
422
|
+
expect(events[0]).toMatchObject({ type: "error", category: "auth" });
|
|
423
|
+
expect(events[1]).toEqual({ type: "closed" });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("a load-shedding close maps to rate-limit", async () => {
|
|
427
|
+
const { events } = await startSession();
|
|
428
|
+
|
|
429
|
+
mockWs.simulateClose(1013, "try again later");
|
|
430
|
+
|
|
431
|
+
expect(events[0]).toMatchObject({
|
|
432
|
+
type: "error",
|
|
433
|
+
category: "rate-limit",
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("any other unexpected close maps to provider-error", async () => {
|
|
438
|
+
const { events } = await startSession();
|
|
439
|
+
|
|
440
|
+
mockWs.simulateClose(1006, "abnormal");
|
|
441
|
+
|
|
442
|
+
expect(events[0]).toMatchObject({
|
|
443
|
+
type: "error",
|
|
444
|
+
category: "provider-error",
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("a socket error emits provider-error then closed", async () => {
|
|
449
|
+
const { events } = await startSession();
|
|
450
|
+
|
|
451
|
+
mockWs.simulateError(new Error("connection reset"));
|
|
452
|
+
|
|
453
|
+
expect(events[0]).toMatchObject({
|
|
454
|
+
type: "error",
|
|
455
|
+
category: "provider-error",
|
|
456
|
+
});
|
|
457
|
+
expect(events[1]).toEqual({ type: "closed" });
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("a normal close after a fatal Error frame adds no second error", async () => {
|
|
461
|
+
const { events } = await startSession();
|
|
462
|
+
|
|
463
|
+
mockWs.simulateMessage(
|
|
464
|
+
JSON.stringify({
|
|
465
|
+
type: "Error",
|
|
466
|
+
code: "INVALID_AUTH",
|
|
467
|
+
description: "Invalid credentials",
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
mockWs.simulateClose(1000, "closing");
|
|
471
|
+
|
|
472
|
+
expect(events).toHaveLength(2);
|
|
473
|
+
expect(events[0]).toMatchObject({
|
|
474
|
+
type: "error",
|
|
475
|
+
category: "auth",
|
|
476
|
+
message: "Deepgram Flux error (INVALID_AUTH): Invalid credentials",
|
|
477
|
+
});
|
|
478
|
+
expect(events[1]).toEqual({ type: "closed" });
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("an abnormal close after a fatal Error frame adds no second error", async () => {
|
|
482
|
+
const { events } = await startSession();
|
|
483
|
+
|
|
484
|
+
mockWs.simulateMessage(
|
|
485
|
+
JSON.stringify({
|
|
486
|
+
type: "Error",
|
|
487
|
+
code: "RATE_LIMIT_EXCEEDED",
|
|
488
|
+
description: "Too many requests",
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
mockWs.simulateClose(1006, "abnormal");
|
|
492
|
+
|
|
493
|
+
expect(events).toHaveLength(2);
|
|
494
|
+
expect(events[0]).toMatchObject({
|
|
495
|
+
type: "error",
|
|
496
|
+
category: "rate-limit",
|
|
497
|
+
message: "Deepgram Flux error (RATE_LIMIT_EXCEEDED): Too many requests",
|
|
498
|
+
});
|
|
499
|
+
expect(events[1]).toEqual({ type: "closed" });
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("a socket error after a fatal Error frame adds no second error", async () => {
|
|
503
|
+
const { events } = await startSession();
|
|
504
|
+
|
|
505
|
+
mockWs.simulateMessage(
|
|
506
|
+
JSON.stringify({
|
|
507
|
+
type: "Error",
|
|
508
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
509
|
+
description: "Something broke",
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
mockWs.simulateError(new Error("connection reset"));
|
|
513
|
+
|
|
514
|
+
expect(events).toHaveLength(2);
|
|
515
|
+
expect(events[0]).toMatchObject({ type: "error" });
|
|
516
|
+
expect(events[1]).toEqual({ type: "closed" });
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("the inactivity watchdog only fires while audio awaits a response", async () => {
|
|
520
|
+
const { transcriber, events } = await startSession({
|
|
521
|
+
inactivityTimeoutMs: 20,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Idle with nothing owed: the watchdog re-arms rather than firing.
|
|
525
|
+
await Bun.sleep(60);
|
|
526
|
+
expect(events).toEqual([]);
|
|
527
|
+
|
|
528
|
+
transcriber.sendAudio(Buffer.from([1, 2]), "audio/pcm");
|
|
529
|
+
await Bun.sleep(80);
|
|
530
|
+
|
|
531
|
+
expect(events[0]).toMatchObject({ type: "error", category: "timeout" });
|
|
532
|
+
expect(events.at(-1)).toEqual({ type: "closed" });
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// ─────────────────────────────────────────────────────────────────
|
|
537
|
+
// Lifecycle
|
|
538
|
+
// ─────────────────────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
describe("lifecycle", () => {
|
|
541
|
+
test("start() throws when called twice", async () => {
|
|
542
|
+
const { transcriber } = await startSession();
|
|
543
|
+
|
|
544
|
+
await expect(transcriber.start(() => {})).rejects.toThrow(
|
|
545
|
+
"start() called twice",
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("stop() sends CloseStream and reports closed once the socket closes", async () => {
|
|
550
|
+
const { transcriber, events } = await startSession();
|
|
551
|
+
|
|
552
|
+
transcriber.stop();
|
|
553
|
+
|
|
554
|
+
expect(mockWs.sentData).toEqual([
|
|
555
|
+
JSON.stringify({ type: "CloseStream" }),
|
|
556
|
+
]);
|
|
557
|
+
expect(events).toEqual([]);
|
|
558
|
+
|
|
559
|
+
mockWs.simulateClose(1000, "");
|
|
560
|
+
expect(events).toEqual([{ type: "closed" }]);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("stop() is idempotent and closed is emitted exactly once", async () => {
|
|
564
|
+
const { transcriber, events } = await startSession();
|
|
565
|
+
|
|
566
|
+
transcriber.stop();
|
|
567
|
+
mockWs.simulateClose(1000, "");
|
|
568
|
+
transcriber.stop();
|
|
569
|
+
mockWs.simulateClose(1000, "");
|
|
570
|
+
|
|
571
|
+
expect(events).toEqual([{ type: "closed" }]);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("keepalive frames go out on the configured interval", async () => {
|
|
575
|
+
const { transcriber } = await startSession({ keepaliveIntervalMs: 10 });
|
|
576
|
+
|
|
577
|
+
await Bun.sleep(35);
|
|
578
|
+
transcriber.stop();
|
|
579
|
+
|
|
580
|
+
const keepalives = mockWs.sentData.filter(
|
|
581
|
+
(data) => data === JSON.stringify({ type: "KeepAlive" }),
|
|
582
|
+
);
|
|
583
|
+
expect(keepalives.length).toBeGreaterThanOrEqual(2);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("finalizeUtterance is absent, Flux has no mid-stream flush", async () => {
|
|
587
|
+
const { transcriber } = await startSession();
|
|
588
|
+
|
|
589
|
+
// Flux commits a transcript only on EndOfTurn and its protocol has no
|
|
590
|
+
// flush that keeps the socket open, so a finalizeUtterance here could
|
|
591
|
+
// only claim a commit that never happened and lose the tail of every
|
|
592
|
+
// turn released on a caller-side boundary. Callers feature-detect the
|
|
593
|
+
// method and fall back to stop(). The contract type is where the
|
|
594
|
+
// optional method lives, so the check goes through it.
|
|
595
|
+
const asContract: StreamingTranscriber = transcriber;
|
|
596
|
+
expect(asContract.finalizeUtterance).toBeUndefined();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("audio sent after stop() never reaches the socket", async () => {
|
|
600
|
+
const { transcriber } = await startSession();
|
|
601
|
+
|
|
602
|
+
transcriber.stop();
|
|
603
|
+
mockWs.sentData.length = 0;
|
|
604
|
+
transcriber.sendAudio(Buffer.from([1, 2]), "audio/pcm");
|
|
605
|
+
|
|
606
|
+
expect(mockWs.sentData).toEqual([]);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// ─────────────────────────────────────────────────────────────────
|
|
611
|
+
// Identity
|
|
612
|
+
// ─────────────────────────────────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
test("reports the deepgram-flux provider on the daemon-streaming boundary", () => {
|
|
615
|
+
const transcriber = new DeepgramFluxRealtimeTranscriber(TEST_API_KEY);
|
|
616
|
+
|
|
617
|
+
expect(transcriber.providerId).toBe("deepgram-flux");
|
|
618
|
+
expect(transcriber.boundaryId).toBe("daemon-streaming");
|
|
619
|
+
});
|
|
620
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
batchBoundaryGapReason,
|
|
4
5
|
getCredentialProvider,
|
|
5
6
|
getProviderEntry,
|
|
6
7
|
listCredentialProviderNames,
|
|
@@ -23,6 +24,7 @@ describe("STT provider catalog", () => {
|
|
|
23
24
|
const ids = listProviderIds();
|
|
24
25
|
expect(ids).toContain("openai-whisper");
|
|
25
26
|
expect(ids).toContain("deepgram");
|
|
27
|
+
expect(ids).toContain("deepgram-flux");
|
|
26
28
|
expect(ids).toContain("google-gemini");
|
|
27
29
|
});
|
|
28
30
|
|
|
@@ -101,6 +103,27 @@ describe("STT provider catalog", () => {
|
|
|
101
103
|
expect(supportsBoundary("openai-whisper", "daemon-streaming")).toBe(true);
|
|
102
104
|
});
|
|
103
105
|
|
|
106
|
+
test("deepgram-flux is streaming-only", () => {
|
|
107
|
+
// Flux has no batch endpoint; the batch transcriber factory rejects it
|
|
108
|
+
// explicitly rather than silently falling through.
|
|
109
|
+
expect(supportsBoundary("deepgram-flux", "daemon-streaming")).toBe(true);
|
|
110
|
+
expect(supportsBoundary("deepgram-flux", "daemon-batch")).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("batchBoundaryGapReason names the batch provider on the same credential", () => {
|
|
114
|
+
const reason = batchBoundaryGapReason("deepgram-flux");
|
|
115
|
+
expect(reason).toBe(
|
|
116
|
+
'Deepgram Flux is streaming-only. Batch transcription requires the deepgram provider: set services.stt.provider to "deepgram".',
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("batchBoundaryGapReason falls back to generic guidance for an unknown provider", () => {
|
|
121
|
+
// No catalog entry means no credential to search a batch-capable peer on.
|
|
122
|
+
const reason = batchBoundaryGapReason("nonexistent" as never);
|
|
123
|
+
expect(reason).toContain("nonexistent is streaming-only");
|
|
124
|
+
expect(reason).toContain("supports batch transcription");
|
|
125
|
+
});
|
|
126
|
+
|
|
104
127
|
test("supportsBoundary returns false for unknown provider IDs", () => {
|
|
105
128
|
// Cast to bypass type checking for the test
|
|
106
129
|
expect(supportsBoundary("nonexistent" as never, "daemon-batch")).toBe(
|
|
@@ -150,6 +173,8 @@ describe("STT provider catalog", () => {
|
|
|
150
173
|
["deepgram", "manual"],
|
|
151
174
|
["vellum", "manual"],
|
|
152
175
|
["xai", "manual"],
|
|
176
|
+
// Flux takes no language parameter: its model is monolingual English.
|
|
177
|
+
["deepgram-flux", "auto"],
|
|
153
178
|
["google-gemini", "auto"],
|
|
154
179
|
["openai-whisper", "auto"],
|
|
155
180
|
] as const;
|
|
@@ -171,6 +196,15 @@ describe("STT provider catalog", () => {
|
|
|
171
196
|
expect(getCredentialProvider("google-gemini")).toBe("gemini");
|
|
172
197
|
});
|
|
173
198
|
|
|
199
|
+
test("deepgram-flux shares the deepgram credential", () => {
|
|
200
|
+
expect(getCredentialProvider("deepgram-flux")).toBe("deepgram");
|
|
201
|
+
// Sharing must not duplicate the key in the API-key provider list.
|
|
202
|
+
const deepgramEntries = listCredentialProviderNames().filter(
|
|
203
|
+
(name) => name === "deepgram",
|
|
204
|
+
);
|
|
205
|
+
expect(deepgramEntries).toEqual(["deepgram"]);
|
|
206
|
+
});
|
|
207
|
+
|
|
174
208
|
test("getCredentialProvider returns undefined for unknown ID", () => {
|
|
175
209
|
expect(getCredentialProvider("nonexistent" as never)).toBeUndefined();
|
|
176
210
|
});
|