@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,1234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
jest,
|
|
7
|
+
mock,
|
|
8
|
+
test,
|
|
9
|
+
} from "bun:test";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Module mocks — declared before imports of the module under test.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
// Mock the STT resolve module (used by MediaStreamSttSession)
|
|
16
|
+
mock.module("../providers/speech-to-text/resolve.js", () => ({
|
|
17
|
+
resolveTelephonySttCapability: jest.fn(),
|
|
18
|
+
resolveBatchTranscriber: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock the logger to suppress output during tests
|
|
22
|
+
mock.module("../util/logger.js", () => ({
|
|
23
|
+
getLogger: () => ({
|
|
24
|
+
info: () => {},
|
|
25
|
+
warn: () => {},
|
|
26
|
+
error: () => {},
|
|
27
|
+
debug: () => {},
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// Mock the call store — lightweight in-memory stubs
|
|
32
|
+
const mockSessions = new Map<string, Record<string, unknown>>();
|
|
33
|
+
const mockEvents: Array<{
|
|
34
|
+
callSessionId: string;
|
|
35
|
+
eventType: string;
|
|
36
|
+
data: unknown;
|
|
37
|
+
}> = [];
|
|
38
|
+
|
|
39
|
+
mock.module("../calls/call-store.js", () => ({
|
|
40
|
+
getCallSession: jest.fn((id: string) => mockSessions.get(id) ?? null),
|
|
41
|
+
updateCallSession: jest.fn((id: string, updates: Record<string, unknown>) => {
|
|
42
|
+
const session = mockSessions.get(id);
|
|
43
|
+
if (session) {
|
|
44
|
+
Object.assign(session, updates);
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
recordCallEvent: jest.fn(
|
|
48
|
+
(callSessionId: string, eventType: string, data: unknown) => {
|
|
49
|
+
mockEvents.push({ callSessionId, eventType, data });
|
|
50
|
+
},
|
|
51
|
+
),
|
|
52
|
+
createCallSession: jest.fn(),
|
|
53
|
+
getCallSessionByCallSid: jest.fn(),
|
|
54
|
+
getActiveCallSessionForConversation: jest.fn(),
|
|
55
|
+
createPendingQuestion: jest.fn(),
|
|
56
|
+
expirePendingQuestions: jest.fn(),
|
|
57
|
+
getPendingQuestion: jest.fn(),
|
|
58
|
+
answerPendingQuestion: jest.fn(),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Mock the call state machine
|
|
62
|
+
mock.module("../calls/call-state-machine.js", () => ({
|
|
63
|
+
isTerminalState: jest.fn(
|
|
64
|
+
(status: string) =>
|
|
65
|
+
status === "completed" || status === "failed" || status === "cancelled",
|
|
66
|
+
),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// Mock the call state (controller registry)
|
|
70
|
+
const mockControllers = new Map<string, unknown>();
|
|
71
|
+
mock.module("../calls/call-state.js", () => ({
|
|
72
|
+
registerCallController: jest.fn(
|
|
73
|
+
(callSessionId: string, controller: unknown) => {
|
|
74
|
+
mockControllers.set(callSessionId, controller);
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
unregisterCallController: jest.fn((callSessionId: string) => {
|
|
78
|
+
mockControllers.delete(callSessionId);
|
|
79
|
+
}),
|
|
80
|
+
getCallController: jest.fn((callSessionId: string) =>
|
|
81
|
+
mockControllers.get(callSessionId),
|
|
82
|
+
),
|
|
83
|
+
fireCallTranscriptNotifier: jest.fn(),
|
|
84
|
+
fireCallQuestionNotifier: jest.fn(),
|
|
85
|
+
fireCallCompletionNotifier: jest.fn(),
|
|
86
|
+
registerCallQuestionNotifier: jest.fn(),
|
|
87
|
+
unregisterCallQuestionNotifier: jest.fn(),
|
|
88
|
+
registerCallTranscriptNotifier: jest.fn(),
|
|
89
|
+
unregisterCallTranscriptNotifier: jest.fn(),
|
|
90
|
+
registerCallCompletionNotifier: jest.fn(),
|
|
91
|
+
unregisterCallCompletionNotifier: jest.fn(),
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
// Mock the finalize-call module
|
|
95
|
+
mock.module("../calls/finalize-call.js", () => ({
|
|
96
|
+
finalizeCall: jest.fn(),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
// Mock the call pointer messages
|
|
100
|
+
mock.module("../calls/call-pointer-messages.js", () => ({
|
|
101
|
+
addPointerMessage: jest.fn(async () => {}),
|
|
102
|
+
formatDuration: jest.fn((ms: number) => `${Math.round(ms / 1000)}s`),
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// Mock the CallController to avoid pulling in the full conversation pipeline
|
|
106
|
+
const mockStartInitialGreeting = jest.fn(async () => {});
|
|
107
|
+
const mockHandleCallerUtterance = jest.fn(async () => {});
|
|
108
|
+
const mockHandleInterrupt = jest.fn();
|
|
109
|
+
const mockDestroy = jest.fn();
|
|
110
|
+
|
|
111
|
+
const mockHandleBargeIn = jest.fn(() => false);
|
|
112
|
+
|
|
113
|
+
mock.module("../calls/call-controller.js", () => ({
|
|
114
|
+
CallController: jest.fn().mockImplementation(() => ({
|
|
115
|
+
startInitialGreeting: mockStartInitialGreeting,
|
|
116
|
+
handleCallerUtterance: mockHandleCallerUtterance,
|
|
117
|
+
handleInterrupt: mockHandleInterrupt,
|
|
118
|
+
handleBargeIn: mockHandleBargeIn,
|
|
119
|
+
destroy: mockDestroy,
|
|
120
|
+
getState: jest.fn(() => "idle"),
|
|
121
|
+
setTrustContext: jest.fn(),
|
|
122
|
+
markNextCallerTurnAsOpeningAck: jest.fn(),
|
|
123
|
+
getPendingConsultationQuestionId: jest.fn(),
|
|
124
|
+
handleUserAnswer: jest.fn(),
|
|
125
|
+
handleUserInstruction: jest.fn(),
|
|
126
|
+
})),
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
// Mock the assistant scope
|
|
130
|
+
mock.module("../runtime/assistant-scope.js", () => ({
|
|
131
|
+
DAEMON_INTERNAL_ASSISTANT_ID: "self",
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// Mock the relay setup router so handleStart() doesn't query the database.
|
|
135
|
+
// Default returns normal_call; individual tests can override via
|
|
136
|
+
// `mockRouteSetupResult` to exercise deny and unsupported-flow branches.
|
|
137
|
+
let mockRouteSetupResult: {
|
|
138
|
+
outcome: { action: string; [key: string]: unknown };
|
|
139
|
+
resolved: {
|
|
140
|
+
assistantId: string;
|
|
141
|
+
isInbound: boolean;
|
|
142
|
+
otherPartyNumber: string;
|
|
143
|
+
actorTrust: { trustClass: string; memberRecord: null };
|
|
144
|
+
};
|
|
145
|
+
} = {
|
|
146
|
+
outcome: { action: "normal_call" as const, isInbound: true },
|
|
147
|
+
resolved: {
|
|
148
|
+
assistantId: "self",
|
|
149
|
+
isInbound: true,
|
|
150
|
+
otherPartyNumber: "+15551234567",
|
|
151
|
+
actorTrust: {
|
|
152
|
+
trustClass: "guardian" as const,
|
|
153
|
+
memberRecord: null,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
mock.module("../calls/relay-setup-router.js", () => ({
|
|
159
|
+
routeSetup: jest.fn(() => mockRouteSetupResult),
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
// Mock the actor trust resolver (used by handleStart to derive trust context)
|
|
163
|
+
mock.module("../runtime/actor-trust-resolver.js", () => ({
|
|
164
|
+
toTrustContext: jest.fn(() => ({
|
|
165
|
+
sourceChannel: "phone",
|
|
166
|
+
trustClass: "guardian",
|
|
167
|
+
})),
|
|
168
|
+
resolveActorTrust: jest.fn(() => ({
|
|
169
|
+
trustClass: "guardian",
|
|
170
|
+
memberRecord: null,
|
|
171
|
+
})),
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
// Mock the call speech output (speakSystemPrompt used in deny/unsupported paths)
|
|
175
|
+
mock.module("../calls/call-speech-output.js", () => ({
|
|
176
|
+
speakSystemPrompt: jest.fn(async () => {}),
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
// Mock scoped approval grants (used in handleTransportClosed and early teardown)
|
|
180
|
+
mock.module("../memory/scoped-approval-grants.js", () => ({
|
|
181
|
+
revokeScopedApprovalGrantsForContext: jest.fn(),
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
// Mock the TTS provider resolution so that the dynamic import inside
|
|
185
|
+
// MediaStreamOutput.processSynthesizeItem() doesn't pull in the real
|
|
186
|
+
// config/provider chain (which would hang or error in a test environment).
|
|
187
|
+
mock.module("../calls/resolve-call-tts-provider.js", () => ({
|
|
188
|
+
resolveCallTtsProvider: jest.fn(() => ({
|
|
189
|
+
provider: null,
|
|
190
|
+
useSynthesizedPath: false,
|
|
191
|
+
audioFormat: "mp3" as const,
|
|
192
|
+
})),
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Now import the module under test.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
import { speakSystemPrompt } from "../calls/call-speech-output.js";
|
|
200
|
+
import { registerCallController } from "../calls/call-state.js";
|
|
201
|
+
import { recordCallEvent, updateCallSession } from "../calls/call-store.js";
|
|
202
|
+
import { finalizeCall } from "../calls/finalize-call.js";
|
|
203
|
+
import {
|
|
204
|
+
activeMediaStreamSessions,
|
|
205
|
+
MediaStreamCallSession,
|
|
206
|
+
} from "../calls/media-stream-server.js";
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Mock WebSocket factory
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
function createMockWs() {
|
|
213
|
+
const sent: string[] = [];
|
|
214
|
+
let closed = false;
|
|
215
|
+
let closeCode: number | undefined;
|
|
216
|
+
let closeReason: string | undefined;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
ws: {
|
|
220
|
+
send(data: string) {
|
|
221
|
+
if (closed) throw new Error("WebSocket is closed");
|
|
222
|
+
sent.push(data);
|
|
223
|
+
},
|
|
224
|
+
close(code?: number, reason?: string) {
|
|
225
|
+
closed = true;
|
|
226
|
+
closeCode = code;
|
|
227
|
+
closeReason = reason;
|
|
228
|
+
},
|
|
229
|
+
} as unknown as import("bun").ServerWebSocket<unknown>,
|
|
230
|
+
get sent() {
|
|
231
|
+
return sent;
|
|
232
|
+
},
|
|
233
|
+
get closed() {
|
|
234
|
+
return closed;
|
|
235
|
+
},
|
|
236
|
+
get closeCode() {
|
|
237
|
+
return closeCode;
|
|
238
|
+
},
|
|
239
|
+
get closeReason() {
|
|
240
|
+
return closeReason;
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Fixture factories
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
function makeStartMessage(overrides?: {
|
|
250
|
+
callSid?: string;
|
|
251
|
+
streamSid?: string;
|
|
252
|
+
}): string {
|
|
253
|
+
return JSON.stringify({
|
|
254
|
+
event: "start",
|
|
255
|
+
sequenceNumber: "1",
|
|
256
|
+
streamSid: overrides?.streamSid ?? "MZ00000000000000000000000000000000",
|
|
257
|
+
start: {
|
|
258
|
+
accountSid: "AC00000000000000000000000000000000",
|
|
259
|
+
streamSid: overrides?.streamSid ?? "MZ00000000000000000000000000000000",
|
|
260
|
+
callSid: overrides?.callSid ?? "CA00000000000000000000000000000000",
|
|
261
|
+
tracks: ["inbound"],
|
|
262
|
+
customParameters: {},
|
|
263
|
+
mediaFormat: {
|
|
264
|
+
encoding: "audio/x-mulaw",
|
|
265
|
+
sampleRate: 8000,
|
|
266
|
+
channels: 1,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function makeMediaMessage(payload: string, chunk: string = "1"): string {
|
|
273
|
+
return JSON.stringify({
|
|
274
|
+
event: "media",
|
|
275
|
+
sequenceNumber: "2",
|
|
276
|
+
streamSid: "MZ00000000000000000000000000000000",
|
|
277
|
+
media: {
|
|
278
|
+
track: "inbound",
|
|
279
|
+
chunk,
|
|
280
|
+
timestamp: "100",
|
|
281
|
+
payload,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function makeStopMessage(): string {
|
|
287
|
+
return JSON.stringify({
|
|
288
|
+
event: "stop",
|
|
289
|
+
sequenceNumber: "99",
|
|
290
|
+
streamSid: "MZ00000000000000000000000000000000",
|
|
291
|
+
stop: {
|
|
292
|
+
accountSid: "AC00000000000000000000000000000000",
|
|
293
|
+
callSid: "CA00000000000000000000000000000000",
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function makeMarkMessage(name: string): string {
|
|
299
|
+
return JSON.stringify({
|
|
300
|
+
event: "mark",
|
|
301
|
+
sequenceNumber: "50",
|
|
302
|
+
streamSid: "MZ00000000000000000000000000000000",
|
|
303
|
+
mark: { name },
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Setup / teardown
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
beforeEach(() => {
|
|
312
|
+
jest.useFakeTimers();
|
|
313
|
+
mockSessions.clear();
|
|
314
|
+
mockEvents.length = 0;
|
|
315
|
+
mockControllers.clear();
|
|
316
|
+
activeMediaStreamSessions.clear();
|
|
317
|
+
mockStartInitialGreeting.mockClear();
|
|
318
|
+
mockHandleCallerUtterance.mockClear();
|
|
319
|
+
mockHandleInterrupt.mockClear();
|
|
320
|
+
mockHandleBargeIn.mockClear();
|
|
321
|
+
mockHandleBargeIn.mockReturnValue(false);
|
|
322
|
+
mockDestroy.mockClear();
|
|
323
|
+
(registerCallController as jest.Mock).mockClear();
|
|
324
|
+
(recordCallEvent as jest.Mock).mockClear();
|
|
325
|
+
(updateCallSession as jest.Mock).mockClear();
|
|
326
|
+
(finalizeCall as jest.Mock).mockClear();
|
|
327
|
+
(speakSystemPrompt as jest.Mock).mockClear();
|
|
328
|
+
// Reset routeSetup to default normal_call
|
|
329
|
+
mockRouteSetupResult = {
|
|
330
|
+
outcome: { action: "normal_call" as const, isInbound: true },
|
|
331
|
+
resolved: {
|
|
332
|
+
assistantId: "self",
|
|
333
|
+
isInbound: true,
|
|
334
|
+
otherPartyNumber: "+15551234567",
|
|
335
|
+
actorTrust: {
|
|
336
|
+
trustClass: "guardian" as const,
|
|
337
|
+
memberRecord: null,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
afterEach(() => {
|
|
344
|
+
jest.useRealTimers();
|
|
345
|
+
activeMediaStreamSessions.clear();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Tests
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
describe("MediaStreamCallSession", () => {
|
|
353
|
+
test("creates a session and exposes output adapter", () => {
|
|
354
|
+
const { ws } = createMockWs();
|
|
355
|
+
const session = new MediaStreamCallSession(ws, "call-1");
|
|
356
|
+
expect(session.callSessionId).toBe("call-1");
|
|
357
|
+
expect(session.getOutput()).toBeDefined();
|
|
358
|
+
expect(session.getOutput().getConnectionState()).toBe("connected");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("start event handling", () => {
|
|
362
|
+
test("start event registers a controller and records call_connected", () => {
|
|
363
|
+
const mock = createMockWs();
|
|
364
|
+
// Set up a call session in the mock store
|
|
365
|
+
mockSessions.set("call-1", {
|
|
366
|
+
id: "call-1",
|
|
367
|
+
conversationId: "conv-1",
|
|
368
|
+
status: "initiated",
|
|
369
|
+
task: "Test task",
|
|
370
|
+
startedAt: null,
|
|
371
|
+
toNumber: "+15551234567",
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
375
|
+
session.handleMessage(makeStartMessage());
|
|
376
|
+
|
|
377
|
+
// Controller should have been registered
|
|
378
|
+
expect(registerCallController).toHaveBeenCalledWith(
|
|
379
|
+
"call-1",
|
|
380
|
+
expect.anything(),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// call_connected event should have been recorded
|
|
384
|
+
expect(recordCallEvent).toHaveBeenCalledWith(
|
|
385
|
+
"call-1",
|
|
386
|
+
"call_connected",
|
|
387
|
+
expect.objectContaining({
|
|
388
|
+
callSid: "CA00000000000000000000000000000000",
|
|
389
|
+
transport: "media-stream",
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Call session should have been updated
|
|
394
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
395
|
+
"call-1",
|
|
396
|
+
expect.objectContaining({
|
|
397
|
+
providerCallSid: "CA00000000000000000000000000000000",
|
|
398
|
+
status: "in_progress",
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// Initial greeting should have been fired
|
|
403
|
+
expect(mockStartInitialGreeting).toHaveBeenCalled();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("start event updates streamSid on the output adapter", () => {
|
|
407
|
+
const mock = createMockWs();
|
|
408
|
+
mockSessions.set("call-1", {
|
|
409
|
+
id: "call-1",
|
|
410
|
+
conversationId: "conv-1",
|
|
411
|
+
status: "initiated",
|
|
412
|
+
task: null,
|
|
413
|
+
startedAt: null,
|
|
414
|
+
toNumber: "+15551234567",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
418
|
+
session.handleMessage(makeStartMessage({ streamSid: "MZ-custom-sid" }));
|
|
419
|
+
|
|
420
|
+
expect(session.getOutput().getStreamSid()).toBe("MZ-custom-sid");
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe("transport close handling", () => {
|
|
425
|
+
test("normal close (1000) marks session as completed", () => {
|
|
426
|
+
const mock = createMockWs();
|
|
427
|
+
mockSessions.set("call-1", {
|
|
428
|
+
id: "call-1",
|
|
429
|
+
conversationId: "conv-1",
|
|
430
|
+
status: "in_progress",
|
|
431
|
+
startedAt: Date.now() - 60000,
|
|
432
|
+
toNumber: "+15551234567",
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
436
|
+
session.handleTransportClosed(1000, "normal-close");
|
|
437
|
+
|
|
438
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
439
|
+
"call-1",
|
|
440
|
+
expect.objectContaining({ status: "completed" }),
|
|
441
|
+
);
|
|
442
|
+
expect(finalizeCall).toHaveBeenCalledWith("call-1", "conv-1");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("abnormal close marks session as failed", () => {
|
|
446
|
+
const mock = createMockWs();
|
|
447
|
+
mockSessions.set("call-1", {
|
|
448
|
+
id: "call-1",
|
|
449
|
+
conversationId: "conv-1",
|
|
450
|
+
status: "in_progress",
|
|
451
|
+
startedAt: Date.now() - 60000,
|
|
452
|
+
toNumber: "+15551234567",
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
456
|
+
session.handleTransportClosed(1006, "abnormal-close");
|
|
457
|
+
|
|
458
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
459
|
+
"call-1",
|
|
460
|
+
expect.objectContaining({
|
|
461
|
+
status: "failed",
|
|
462
|
+
lastError: expect.stringContaining("abnormal-close"),
|
|
463
|
+
}),
|
|
464
|
+
);
|
|
465
|
+
expect(finalizeCall).toHaveBeenCalledWith("call-1", "conv-1");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("close on already-terminal session is a no-op", () => {
|
|
469
|
+
const mock = createMockWs();
|
|
470
|
+
mockSessions.set("call-1", {
|
|
471
|
+
id: "call-1",
|
|
472
|
+
conversationId: "conv-1",
|
|
473
|
+
status: "completed",
|
|
474
|
+
toNumber: "+15551234567",
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
478
|
+
session.handleTransportClosed(1000);
|
|
479
|
+
|
|
480
|
+
// updateCallSession should NOT have been called because session
|
|
481
|
+
// was already terminal
|
|
482
|
+
expect(updateCallSession).not.toHaveBeenCalled();
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe("destroy", () => {
|
|
487
|
+
test("destroys the controller and marks output as closed", () => {
|
|
488
|
+
const mock = createMockWs();
|
|
489
|
+
mockSessions.set("call-1", {
|
|
490
|
+
id: "call-1",
|
|
491
|
+
conversationId: "conv-1",
|
|
492
|
+
status: "initiated",
|
|
493
|
+
task: null,
|
|
494
|
+
startedAt: null,
|
|
495
|
+
toNumber: "+15551234567",
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
499
|
+
// Trigger start to create a controller
|
|
500
|
+
session.handleMessage(makeStartMessage());
|
|
501
|
+
|
|
502
|
+
session.destroy();
|
|
503
|
+
expect(mockDestroy).toHaveBeenCalled();
|
|
504
|
+
expect(session.getOutput().getConnectionState()).toBe("closed");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("destroy is idempotent", () => {
|
|
508
|
+
const mock = createMockWs();
|
|
509
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
510
|
+
session.destroy();
|
|
511
|
+
session.destroy(); // Should not throw
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("messages after destroy are dropped", () => {
|
|
515
|
+
const mock = createMockWs();
|
|
516
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
517
|
+
session.destroy();
|
|
518
|
+
|
|
519
|
+
// Should not throw or create side effects
|
|
520
|
+
session.handleMessage(makeStartMessage());
|
|
521
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("media event forwarding", () => {
|
|
526
|
+
test("media events are forwarded to the STT session without errors", () => {
|
|
527
|
+
const mock = createMockWs();
|
|
528
|
+
mockSessions.set("call-1", {
|
|
529
|
+
id: "call-1",
|
|
530
|
+
conversationId: "conv-1",
|
|
531
|
+
status: "initiated",
|
|
532
|
+
task: null,
|
|
533
|
+
startedAt: null,
|
|
534
|
+
toNumber: "+15551234567",
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
538
|
+
session.handleMessage(makeStartMessage());
|
|
539
|
+
|
|
540
|
+
// Send media frames — should not throw
|
|
541
|
+
const payload = Buffer.from("test-audio").toString("base64");
|
|
542
|
+
session.handleMessage(makeMediaMessage(payload, "1"));
|
|
543
|
+
session.handleMessage(makeMediaMessage(payload, "2"));
|
|
544
|
+
session.handleMessage(makeMediaMessage(payload, "3"));
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("mark events are forwarded without errors", () => {
|
|
548
|
+
const mock = createMockWs();
|
|
549
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
550
|
+
|
|
551
|
+
// Mark events should be silently handled
|
|
552
|
+
session.handleMessage(makeMarkMessage("end-of-turn"));
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test("stop events are forwarded to the STT session", () => {
|
|
556
|
+
const mock = createMockWs();
|
|
557
|
+
mockSessions.set("call-1", {
|
|
558
|
+
id: "call-1",
|
|
559
|
+
conversationId: "conv-1",
|
|
560
|
+
status: "initiated",
|
|
561
|
+
task: null,
|
|
562
|
+
startedAt: null,
|
|
563
|
+
toNumber: "+15551234567",
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
567
|
+
session.handleMessage(makeStartMessage());
|
|
568
|
+
session.handleMessage(makeStopMessage());
|
|
569
|
+
|
|
570
|
+
// Stop is informational; the session continues until WebSocket closes
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe("malformed messages", () => {
|
|
575
|
+
test("invalid JSON is dropped silently", () => {
|
|
576
|
+
const mock = createMockWs();
|
|
577
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
578
|
+
// Should not throw
|
|
579
|
+
session.handleMessage("not json {{{");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("unknown event types are dropped silently", () => {
|
|
583
|
+
const mock = createMockWs();
|
|
584
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
585
|
+
session.handleMessage(JSON.stringify({ event: "unknown_type" }));
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe("media-stream output egress", () => {
|
|
591
|
+
// These tests exercise the async playback queue which relies on real
|
|
592
|
+
// timers (setTimeout / Bun.sleep). Override the global fake-timers
|
|
593
|
+
// from the outer beforeEach for this block.
|
|
594
|
+
beforeEach(() => {
|
|
595
|
+
jest.useRealTimers();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("sendTextToken with text produces outbound media frames", async () => {
|
|
599
|
+
const mockWs = createMockWs();
|
|
600
|
+
mockSessions.set("call-out-1", {
|
|
601
|
+
id: "call-out-1",
|
|
602
|
+
conversationId: "conv-out-1",
|
|
603
|
+
status: "initiated",
|
|
604
|
+
task: "Outbound test",
|
|
605
|
+
startedAt: null,
|
|
606
|
+
toNumber: "+15551234567",
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-out-1");
|
|
610
|
+
session.handleMessage(makeStartMessage());
|
|
611
|
+
|
|
612
|
+
// Simulate the controller sending text to the output adapter
|
|
613
|
+
const output = session.getOutput();
|
|
614
|
+
output.sendTextToken("Hello caller", true);
|
|
615
|
+
|
|
616
|
+
// Allow the async playback queue to drain
|
|
617
|
+
await Bun.sleep(50);
|
|
618
|
+
|
|
619
|
+
// The output should have sent at least an end-of-turn mark.
|
|
620
|
+
// Media frames depend on TTS provider availability (mocked away in
|
|
621
|
+
// this test suite), but the mark is always sent synchronously.
|
|
622
|
+
const markMessages = mockWs.sent.filter(
|
|
623
|
+
(s) => JSON.parse(s).event === "mark",
|
|
624
|
+
);
|
|
625
|
+
expect(markMessages.length).toBeGreaterThan(0);
|
|
626
|
+
|
|
627
|
+
const markParsed = JSON.parse(markMessages[0]);
|
|
628
|
+
expect(markParsed.mark.name).toBe("end-of-turn");
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("empty sendTextToken (end-of-turn signal) sends only a mark, no media", async () => {
|
|
632
|
+
const mockWs = createMockWs();
|
|
633
|
+
mockSessions.set("call-eot-1", {
|
|
634
|
+
id: "call-eot-1",
|
|
635
|
+
conversationId: "conv-eot-1",
|
|
636
|
+
status: "initiated",
|
|
637
|
+
task: null,
|
|
638
|
+
startedAt: null,
|
|
639
|
+
toNumber: "+15551234567",
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-eot-1");
|
|
643
|
+
session.handleMessage(makeStartMessage());
|
|
644
|
+
|
|
645
|
+
const output = session.getOutput();
|
|
646
|
+
output.sendTextToken("", true);
|
|
647
|
+
|
|
648
|
+
await Bun.sleep(50);
|
|
649
|
+
|
|
650
|
+
// Should send a mark but no media frames
|
|
651
|
+
const mediaMessages = mockWs.sent.filter(
|
|
652
|
+
(s) => JSON.parse(s).event === "media",
|
|
653
|
+
);
|
|
654
|
+
const markMessages = mockWs.sent.filter(
|
|
655
|
+
(s) => JSON.parse(s).event === "mark",
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
expect(mediaMessages).toHaveLength(0);
|
|
659
|
+
expect(markMessages.length).toBeGreaterThan(0);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("sendAudioPayload sends media frames to Twilio", () => {
|
|
663
|
+
const mockWs = createMockWs();
|
|
664
|
+
mockSessions.set("call-audio-1", {
|
|
665
|
+
id: "call-audio-1",
|
|
666
|
+
conversationId: "conv-audio-1",
|
|
667
|
+
status: "initiated",
|
|
668
|
+
task: null,
|
|
669
|
+
startedAt: null,
|
|
670
|
+
toNumber: "+15551234567",
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-audio-1");
|
|
674
|
+
session.handleMessage(makeStartMessage());
|
|
675
|
+
|
|
676
|
+
const output = session.getOutput();
|
|
677
|
+
const payload = Buffer.from("test-audio-data").toString("base64");
|
|
678
|
+
output.sendAudioPayload(payload);
|
|
679
|
+
|
|
680
|
+
const mediaMessages = mockWs.sent.filter(
|
|
681
|
+
(s) => JSON.parse(s).event === "media",
|
|
682
|
+
);
|
|
683
|
+
expect(mediaMessages).toHaveLength(1);
|
|
684
|
+
expect(JSON.parse(mediaMessages[0]).media.payload).toBe(payload);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("clearAudio sends clear command and flushes playback queue", async () => {
|
|
688
|
+
const mockWs = createMockWs();
|
|
689
|
+
mockSessions.set("call-barge-1", {
|
|
690
|
+
id: "call-barge-1",
|
|
691
|
+
conversationId: "conv-barge-1",
|
|
692
|
+
status: "initiated",
|
|
693
|
+
task: null,
|
|
694
|
+
startedAt: null,
|
|
695
|
+
toNumber: "+15551234567",
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-barge-1");
|
|
699
|
+
session.handleMessage(makeStartMessage());
|
|
700
|
+
|
|
701
|
+
const output = session.getOutput();
|
|
702
|
+
|
|
703
|
+
// Queue some output
|
|
704
|
+
output.sendTextToken("This will be interrupted", true);
|
|
705
|
+
|
|
706
|
+
// Immediately barge-in
|
|
707
|
+
output.clearAudio();
|
|
708
|
+
|
|
709
|
+
await Bun.sleep(50);
|
|
710
|
+
|
|
711
|
+
// Should have sent a clear command
|
|
712
|
+
const clearMessages = mockWs.sent.filter(
|
|
713
|
+
(s) => JSON.parse(s).event === "clear",
|
|
714
|
+
);
|
|
715
|
+
expect(clearMessages.length).toBeGreaterThanOrEqual(1);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
test("barge-in via speech start clears audio and interrupts controller", () => {
|
|
719
|
+
const mockWs = createMockWs();
|
|
720
|
+
mockSessions.set("call-interrupt-1", {
|
|
721
|
+
id: "call-interrupt-1",
|
|
722
|
+
conversationId: "conv-interrupt-1",
|
|
723
|
+
status: "initiated",
|
|
724
|
+
task: "Test task",
|
|
725
|
+
startedAt: null,
|
|
726
|
+
toNumber: "+15551234567",
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-interrupt-1");
|
|
730
|
+
session.handleMessage(makeStartMessage());
|
|
731
|
+
|
|
732
|
+
// Verify the controller is created
|
|
733
|
+
expect(session.getController()).not.toBeNull();
|
|
734
|
+
|
|
735
|
+
// Simulate a caller starting to speak (barge-in) by sending media
|
|
736
|
+
// while the assistant would be speaking. The handleSpeechStart callback
|
|
737
|
+
// should clear audio and call handleInterrupt on the controller.
|
|
738
|
+
// Note: In the real flow, the STT session detects speech start from
|
|
739
|
+
// audio energy. Here we verify the wiring by checking that the
|
|
740
|
+
// controller's handleInterrupt was called (if speech start fires).
|
|
741
|
+
// The STT session is stubbed, so we verify the output adapter's
|
|
742
|
+
// clearAudio works independently.
|
|
743
|
+
const output = session.getOutput();
|
|
744
|
+
output.clearAudio();
|
|
745
|
+
|
|
746
|
+
const clearMessages = mockWs.sent.filter(
|
|
747
|
+
(s) => JSON.parse(s).event === "clear",
|
|
748
|
+
);
|
|
749
|
+
expect(clearMessages.length).toBeGreaterThanOrEqual(1);
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
describe("activeMediaStreamSessions registry", () => {
|
|
754
|
+
test("sessions can be added and retrieved", () => {
|
|
755
|
+
const mock = createMockWs();
|
|
756
|
+
const session = new MediaStreamCallSession(mock.ws, "call-1");
|
|
757
|
+
activeMediaStreamSessions.set("call-1", session);
|
|
758
|
+
expect(activeMediaStreamSessions.get("call-1")).toBe(session);
|
|
759
|
+
activeMediaStreamSessions.delete("call-1");
|
|
760
|
+
expect(activeMediaStreamSessions.get("call-1")).toBeUndefined();
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// ---------------------------------------------------------------------------
|
|
765
|
+
// Scenario-driven setup outcome coverage
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
// These tests exercise the deny and unsupported-action branches in
|
|
768
|
+
// MediaStreamCallSession.handleStart by overriding mockRouteSetupResult
|
|
769
|
+
// before sending a start message.
|
|
770
|
+
|
|
771
|
+
describe("media-stream setup outcome scenarios", () => {
|
|
772
|
+
describe("deny outcome", () => {
|
|
773
|
+
test("deny outcome records inbound_acl_denied event and sets status to failed", () => {
|
|
774
|
+
mockRouteSetupResult = {
|
|
775
|
+
outcome: {
|
|
776
|
+
action: "deny",
|
|
777
|
+
message: "This number is not authorized.",
|
|
778
|
+
logReason: "Inbound voice ACL: blocked caller",
|
|
779
|
+
},
|
|
780
|
+
resolved: {
|
|
781
|
+
assistantId: "self",
|
|
782
|
+
isInbound: true,
|
|
783
|
+
otherPartyNumber: "+15559998888",
|
|
784
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const mockWs = createMockWs();
|
|
789
|
+
mockSessions.set("call-deny-1", {
|
|
790
|
+
id: "call-deny-1",
|
|
791
|
+
conversationId: "conv-deny-1",
|
|
792
|
+
status: "initiated",
|
|
793
|
+
task: null,
|
|
794
|
+
startedAt: null,
|
|
795
|
+
fromNumber: "+15559998888",
|
|
796
|
+
toNumber: "+15550001111",
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-deny-1");
|
|
800
|
+
session.handleMessage(makeStartMessage());
|
|
801
|
+
|
|
802
|
+
// Should record an inbound_acl_denied event
|
|
803
|
+
expect(recordCallEvent).toHaveBeenCalledWith(
|
|
804
|
+
"call-deny-1",
|
|
805
|
+
"inbound_acl_denied",
|
|
806
|
+
expect.objectContaining({
|
|
807
|
+
from: "+15559998888",
|
|
808
|
+
}),
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// Should update session to failed
|
|
812
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
813
|
+
"call-deny-1",
|
|
814
|
+
expect.objectContaining({
|
|
815
|
+
status: "failed",
|
|
816
|
+
lastError: "Inbound voice ACL: blocked caller",
|
|
817
|
+
}),
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
// Should NOT register a controller (deny path skips it)
|
|
821
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("deny outcome speaks the denial message", () => {
|
|
825
|
+
mockRouteSetupResult = {
|
|
826
|
+
outcome: {
|
|
827
|
+
action: "deny",
|
|
828
|
+
message: "This number is not authorized to use this assistant.",
|
|
829
|
+
logReason: "Inbound voice ACL: member policy deny",
|
|
830
|
+
},
|
|
831
|
+
resolved: {
|
|
832
|
+
assistantId: "self",
|
|
833
|
+
isInbound: true,
|
|
834
|
+
otherPartyNumber: "+15559998888",
|
|
835
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
836
|
+
},
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
const mockWs = createMockWs();
|
|
840
|
+
mockSessions.set("call-deny-speak-1", {
|
|
841
|
+
id: "call-deny-speak-1",
|
|
842
|
+
conversationId: "conv-deny-speak-1",
|
|
843
|
+
status: "initiated",
|
|
844
|
+
task: null,
|
|
845
|
+
startedAt: null,
|
|
846
|
+
fromNumber: "+15559998888",
|
|
847
|
+
toNumber: "+15550001111",
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
const session = new MediaStreamCallSession(
|
|
851
|
+
mockWs.ws,
|
|
852
|
+
"call-deny-speak-1",
|
|
853
|
+
);
|
|
854
|
+
session.handleMessage(makeStartMessage());
|
|
855
|
+
|
|
856
|
+
// speakSystemPrompt should be called with the denial message
|
|
857
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
858
|
+
expect.anything(),
|
|
859
|
+
"This number is not authorized to use this assistant.",
|
|
860
|
+
);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("deny outcome runs finalization", () => {
|
|
864
|
+
mockRouteSetupResult = {
|
|
865
|
+
outcome: {
|
|
866
|
+
action: "deny",
|
|
867
|
+
message: "Not authorized.",
|
|
868
|
+
logReason: "ACL deny",
|
|
869
|
+
},
|
|
870
|
+
resolved: {
|
|
871
|
+
assistantId: "self",
|
|
872
|
+
isInbound: true,
|
|
873
|
+
otherPartyNumber: "+15559998888",
|
|
874
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
const mockWs = createMockWs();
|
|
879
|
+
mockSessions.set("call-deny-finalize-1", {
|
|
880
|
+
id: "call-deny-finalize-1",
|
|
881
|
+
conversationId: "conv-deny-finalize-1",
|
|
882
|
+
status: "initiated",
|
|
883
|
+
task: null,
|
|
884
|
+
startedAt: null,
|
|
885
|
+
fromNumber: "+15559998888",
|
|
886
|
+
toNumber: "+15550001111",
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
const session = new MediaStreamCallSession(
|
|
890
|
+
mockWs.ws,
|
|
891
|
+
"call-deny-finalize-1",
|
|
892
|
+
);
|
|
893
|
+
session.handleMessage(makeStartMessage());
|
|
894
|
+
|
|
895
|
+
// finalizeCall should be called because early teardown runs it inline
|
|
896
|
+
expect(finalizeCall).toHaveBeenCalledWith(
|
|
897
|
+
"call-deny-finalize-1",
|
|
898
|
+
"conv-deny-finalize-1",
|
|
899
|
+
);
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
describe("unsupported interactive setup flow", () => {
|
|
904
|
+
test("verification outcome records call_failed with preflight-bypass reason", () => {
|
|
905
|
+
mockRouteSetupResult = {
|
|
906
|
+
outcome: {
|
|
907
|
+
action: "verification",
|
|
908
|
+
assistantId: "self",
|
|
909
|
+
fromNumber: "+14155551234",
|
|
910
|
+
},
|
|
911
|
+
resolved: {
|
|
912
|
+
assistantId: "self",
|
|
913
|
+
isInbound: true,
|
|
914
|
+
otherPartyNumber: "+14155551234",
|
|
915
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
916
|
+
},
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
const mockWs = createMockWs();
|
|
920
|
+
mockSessions.set("call-unsup-verify-1", {
|
|
921
|
+
id: "call-unsup-verify-1",
|
|
922
|
+
conversationId: "conv-unsup-verify-1",
|
|
923
|
+
status: "initiated",
|
|
924
|
+
task: null,
|
|
925
|
+
startedAt: null,
|
|
926
|
+
fromNumber: "+14155551234",
|
|
927
|
+
toNumber: "+15550001111",
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const session = new MediaStreamCallSession(
|
|
931
|
+
mockWs.ws,
|
|
932
|
+
"call-unsup-verify-1",
|
|
933
|
+
);
|
|
934
|
+
session.handleMessage(makeStartMessage());
|
|
935
|
+
|
|
936
|
+
// Should record call_failed event with preflight-bypass note
|
|
937
|
+
expect(recordCallEvent).toHaveBeenCalledWith(
|
|
938
|
+
"call-unsup-verify-1",
|
|
939
|
+
"call_failed",
|
|
940
|
+
expect.objectContaining({
|
|
941
|
+
reason: expect.stringContaining("verification"),
|
|
942
|
+
transport: "media-stream",
|
|
943
|
+
}),
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Should set session status to failed
|
|
947
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
948
|
+
"call-unsup-verify-1",
|
|
949
|
+
expect.objectContaining({
|
|
950
|
+
status: "failed",
|
|
951
|
+
lastError: expect.stringContaining("preflight guard"),
|
|
952
|
+
}),
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
// Should NOT register a controller
|
|
956
|
+
expect(registerCallController).not.toHaveBeenCalled();
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
test("name_capture outcome speaks generic apology and tears down", () => {
|
|
960
|
+
mockRouteSetupResult = {
|
|
961
|
+
outcome: {
|
|
962
|
+
action: "name_capture",
|
|
963
|
+
assistantId: "self",
|
|
964
|
+
fromNumber: "+14155551234",
|
|
965
|
+
},
|
|
966
|
+
resolved: {
|
|
967
|
+
assistantId: "self",
|
|
968
|
+
isInbound: true,
|
|
969
|
+
otherPartyNumber: "+14155551234",
|
|
970
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
const mockWs = createMockWs();
|
|
975
|
+
mockSessions.set("call-unsup-name-1", {
|
|
976
|
+
id: "call-unsup-name-1",
|
|
977
|
+
conversationId: "conv-unsup-name-1",
|
|
978
|
+
status: "initiated",
|
|
979
|
+
task: null,
|
|
980
|
+
startedAt: null,
|
|
981
|
+
fromNumber: "+14155551234",
|
|
982
|
+
toNumber: "+15550001111",
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
const session = new MediaStreamCallSession(
|
|
986
|
+
mockWs.ws,
|
|
987
|
+
"call-unsup-name-1",
|
|
988
|
+
);
|
|
989
|
+
session.handleMessage(makeStartMessage());
|
|
990
|
+
|
|
991
|
+
// speakSystemPrompt should be called with the generic apology
|
|
992
|
+
expect(speakSystemPrompt).toHaveBeenCalledWith(
|
|
993
|
+
expect.anything(),
|
|
994
|
+
expect.stringContaining("additional verification"),
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
// Should run finalization inline
|
|
998
|
+
expect(finalizeCall).toHaveBeenCalledWith(
|
|
999
|
+
"call-unsup-name-1",
|
|
1000
|
+
"conv-unsup-name-1",
|
|
1001
|
+
);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test("callee_verification outcome fails with explicit reason", () => {
|
|
1005
|
+
mockRouteSetupResult = {
|
|
1006
|
+
outcome: {
|
|
1007
|
+
action: "callee_verification",
|
|
1008
|
+
verificationConfig: { maxAttempts: 3, codeLength: 6 },
|
|
1009
|
+
},
|
|
1010
|
+
resolved: {
|
|
1011
|
+
assistantId: "self",
|
|
1012
|
+
isInbound: false,
|
|
1013
|
+
otherPartyNumber: "+14155551234",
|
|
1014
|
+
actorTrust: { trustClass: "guardian", memberRecord: null },
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const mockWs = createMockWs();
|
|
1019
|
+
mockSessions.set("call-unsup-callee-1", {
|
|
1020
|
+
id: "call-unsup-callee-1",
|
|
1021
|
+
conversationId: "conv-unsup-callee-1",
|
|
1022
|
+
status: "initiated",
|
|
1023
|
+
task: null,
|
|
1024
|
+
startedAt: null,
|
|
1025
|
+
fromNumber: "+15550001111",
|
|
1026
|
+
toNumber: "+14155551234",
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
const session = new MediaStreamCallSession(
|
|
1030
|
+
mockWs.ws,
|
|
1031
|
+
"call-unsup-callee-1",
|
|
1032
|
+
);
|
|
1033
|
+
session.handleMessage(makeStartMessage());
|
|
1034
|
+
|
|
1035
|
+
// Should record the failure with the specific action
|
|
1036
|
+
expect(recordCallEvent).toHaveBeenCalledWith(
|
|
1037
|
+
"call-unsup-callee-1",
|
|
1038
|
+
"call_failed",
|
|
1039
|
+
expect.objectContaining({
|
|
1040
|
+
reason: expect.stringContaining("callee_verification"),
|
|
1041
|
+
}),
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
// Session should be failed
|
|
1045
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
1046
|
+
"call-unsup-callee-1",
|
|
1047
|
+
expect.objectContaining({ status: "failed" }),
|
|
1048
|
+
);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
test("normal_call after deny scenario still creates controller", () => {
|
|
1052
|
+
// Verify that after a deny-scenario test, resetting to normal_call
|
|
1053
|
+
// properly creates a controller (no cross-test pollution).
|
|
1054
|
+
mockRouteSetupResult = {
|
|
1055
|
+
outcome: { action: "normal_call", isInbound: true },
|
|
1056
|
+
resolved: {
|
|
1057
|
+
assistantId: "self",
|
|
1058
|
+
isInbound: true,
|
|
1059
|
+
otherPartyNumber: "+15551234567",
|
|
1060
|
+
actorTrust: { trustClass: "guardian", memberRecord: null },
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
const mockWs = createMockWs();
|
|
1065
|
+
mockSessions.set("call-reset-1", {
|
|
1066
|
+
id: "call-reset-1",
|
|
1067
|
+
conversationId: "conv-reset-1",
|
|
1068
|
+
status: "initiated",
|
|
1069
|
+
task: "Test task",
|
|
1070
|
+
startedAt: null,
|
|
1071
|
+
toNumber: "+15551234567",
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-reset-1");
|
|
1075
|
+
session.handleMessage(makeStartMessage());
|
|
1076
|
+
|
|
1077
|
+
// Controller should be registered for normal calls
|
|
1078
|
+
expect(registerCallController).toHaveBeenCalledWith(
|
|
1079
|
+
"call-reset-1",
|
|
1080
|
+
expect.anything(),
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
// Initial greeting should fire
|
|
1084
|
+
expect(mockStartInitialGreeting).toHaveBeenCalled();
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// ── Barge-in regression ──────────────────────────────────────────
|
|
1089
|
+
|
|
1090
|
+
describe("barge-in gating", () => {
|
|
1091
|
+
test("immediate inbound audio after stream start does not trigger handleInterrupt", () => {
|
|
1092
|
+
const mockWs = createMockWs();
|
|
1093
|
+
mockSessions.set("call-bargein-1", {
|
|
1094
|
+
id: "call-bargein-1",
|
|
1095
|
+
conversationId: "conv-bargein-1",
|
|
1096
|
+
status: "initiated",
|
|
1097
|
+
task: null,
|
|
1098
|
+
startedAt: null,
|
|
1099
|
+
toNumber: "+15551234567",
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-bargein-1");
|
|
1103
|
+
|
|
1104
|
+
// Stream start bootstraps the controller
|
|
1105
|
+
session.handleMessage(makeStartMessage());
|
|
1106
|
+
expect(mockStartInitialGreeting).toHaveBeenCalled();
|
|
1107
|
+
|
|
1108
|
+
// Immediate inbound audio (speech-like payloads) — before the
|
|
1109
|
+
// assistant has spoken. The speech detector classifies these as
|
|
1110
|
+
// speech, so onSpeechStart fires and calls handleBargeIn. Since
|
|
1111
|
+
// the controller mock returns false (not speaking), handleInterrupt
|
|
1112
|
+
// should NOT be called.
|
|
1113
|
+
const speechPayload = Buffer.alloc(160, 0x00).toString("base64");
|
|
1114
|
+
session.handleMessage(makeMediaMessage(speechPayload, "1"));
|
|
1115
|
+
session.handleMessage(makeMediaMessage(speechPayload, "2"));
|
|
1116
|
+
session.handleMessage(makeMediaMessage(speechPayload, "3"));
|
|
1117
|
+
|
|
1118
|
+
// handleBargeIn was called but returned false
|
|
1119
|
+
expect(mockHandleBargeIn).toHaveBeenCalled();
|
|
1120
|
+
expect(mockHandleInterrupt).not.toHaveBeenCalled();
|
|
1121
|
+
|
|
1122
|
+
// voice_session_aborted should NOT appear in recorded events
|
|
1123
|
+
const abortEvents = mockEvents.filter(
|
|
1124
|
+
(e) =>
|
|
1125
|
+
e.callSessionId === "call-bargein-1" &&
|
|
1126
|
+
e.eventType === "voice_session_aborted",
|
|
1127
|
+
);
|
|
1128
|
+
expect(abortEvents.length).toBe(0);
|
|
1129
|
+
|
|
1130
|
+
session.destroy();
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
test("barge-in is accepted when controller is speaking", () => {
|
|
1134
|
+
// Configure mock to indicate the controller is speaking
|
|
1135
|
+
mockHandleBargeIn.mockReturnValue(true);
|
|
1136
|
+
|
|
1137
|
+
const mockWs = createMockWs();
|
|
1138
|
+
mockSessions.set("call-bargein-2", {
|
|
1139
|
+
id: "call-bargein-2",
|
|
1140
|
+
conversationId: "conv-bargein-2",
|
|
1141
|
+
status: "in_progress",
|
|
1142
|
+
task: null,
|
|
1143
|
+
startedAt: Date.now() - 5000,
|
|
1144
|
+
toNumber: "+15551234567",
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-bargein-2");
|
|
1148
|
+
session.handleMessage(makeStartMessage());
|
|
1149
|
+
|
|
1150
|
+
// Simulate inbound speech audio while assistant is speaking.
|
|
1151
|
+
// Use a high-amplitude mu-law payload so speech detection triggers.
|
|
1152
|
+
const speechPayload = Buffer.alloc(160, 0x00).toString("base64");
|
|
1153
|
+
session.handleMessage(makeMediaMessage(speechPayload, "1"));
|
|
1154
|
+
|
|
1155
|
+
// handleBargeIn should have been called (returning true)
|
|
1156
|
+
expect(mockHandleBargeIn).toHaveBeenCalled();
|
|
1157
|
+
|
|
1158
|
+
session.destroy();
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// ── E2E regression scenario ──────────────────────────────────────
|
|
1163
|
+
|
|
1164
|
+
describe("end-to-end regression: connected call that stays active", () => {
|
|
1165
|
+
test("stream connects, inbound audio starts, call remains active for a turn, controller only destroyed at stop/hangup", () => {
|
|
1166
|
+
const mockWs = createMockWs();
|
|
1167
|
+
mockSessions.set("call-e2e-1", {
|
|
1168
|
+
id: "call-e2e-1",
|
|
1169
|
+
conversationId: "conv-e2e-1",
|
|
1170
|
+
status: "initiated",
|
|
1171
|
+
task: null,
|
|
1172
|
+
startedAt: null,
|
|
1173
|
+
toNumber: "+15551234567",
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
const session = new MediaStreamCallSession(mockWs.ws, "call-e2e-1");
|
|
1177
|
+
|
|
1178
|
+
// 1. Stream connects — start event arrives
|
|
1179
|
+
session.handleMessage(makeStartMessage());
|
|
1180
|
+
expect(registerCallController).toHaveBeenCalledWith(
|
|
1181
|
+
"call-e2e-1",
|
|
1182
|
+
expect.anything(),
|
|
1183
|
+
);
|
|
1184
|
+
expect(mockStartInitialGreeting).toHaveBeenCalled();
|
|
1185
|
+
|
|
1186
|
+
// Verify session was updated to in_progress
|
|
1187
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
1188
|
+
"call-e2e-1",
|
|
1189
|
+
expect.objectContaining({ status: "in_progress" }),
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
// 2. Inbound audio starts immediately (controller idle — barge-in ignored)
|
|
1193
|
+
const payload = Buffer.from("test-audio").toString("base64");
|
|
1194
|
+
for (let i = 1; i <= 5; i++) {
|
|
1195
|
+
session.handleMessage(makeMediaMessage(payload, String(i)));
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// handleInterrupt should NOT have been called (gated barge-in)
|
|
1199
|
+
expect(mockHandleInterrupt).not.toHaveBeenCalled();
|
|
1200
|
+
|
|
1201
|
+
// 3. Controller is NOT destroyed yet — still active
|
|
1202
|
+
expect(mockDestroy).not.toHaveBeenCalled();
|
|
1203
|
+
|
|
1204
|
+
// 4. More media frames arrive (simulating ongoing call)
|
|
1205
|
+
for (let i = 6; i <= 10; i++) {
|
|
1206
|
+
session.handleMessage(makeMediaMessage(payload, String(i)));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Controller still not destroyed
|
|
1210
|
+
expect(mockDestroy).not.toHaveBeenCalled();
|
|
1211
|
+
|
|
1212
|
+
// 5. Stop event arrives — controller should be cleaned up
|
|
1213
|
+
// only when the session is fully destroyed
|
|
1214
|
+
session.handleMessage(makeStopMessage());
|
|
1215
|
+
|
|
1216
|
+
// WebSocket close triggers full teardown
|
|
1217
|
+
mockSessions.set("call-e2e-1", {
|
|
1218
|
+
...mockSessions.get("call-e2e-1")!,
|
|
1219
|
+
status: "in_progress",
|
|
1220
|
+
startedAt: Date.now() - 30000,
|
|
1221
|
+
});
|
|
1222
|
+
session.handleTransportClosed(1000, "normal-close");
|
|
1223
|
+
|
|
1224
|
+
expect(updateCallSession).toHaveBeenCalledWith(
|
|
1225
|
+
"call-e2e-1",
|
|
1226
|
+
expect.objectContaining({ status: "completed" }),
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1229
|
+
// Now destroy
|
|
1230
|
+
session.destroy();
|
|
1231
|
+
expect(mockDestroy).toHaveBeenCalled();
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
});
|