@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
|
@@ -72,6 +72,30 @@ function resolveIngressBaseUrlFromConfig(ingressConfig: unknown): string {
|
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Default routeSetup mock — returns normal_call. Tests that need different
|
|
76
|
+
// outcomes override `mockRouteSetupResult` before calling the handler.
|
|
77
|
+
let mockRouteSetupResult: {
|
|
78
|
+
outcome: { action: string; [key: string]: unknown };
|
|
79
|
+
resolved: {
|
|
80
|
+
assistantId: string;
|
|
81
|
+
isInbound: boolean;
|
|
82
|
+
otherPartyNumber: string;
|
|
83
|
+
actorTrust: { trustClass: string; memberRecord: null };
|
|
84
|
+
};
|
|
85
|
+
} = {
|
|
86
|
+
outcome: { action: "normal_call", isInbound: true },
|
|
87
|
+
resolved: {
|
|
88
|
+
assistantId: "self",
|
|
89
|
+
isInbound: true,
|
|
90
|
+
otherPartyNumber: "+15559998888",
|
|
91
|
+
actorTrust: { trustClass: "guardian", memberRecord: null },
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
mock.module("../calls/relay-setup-router.js", () => ({
|
|
96
|
+
routeSetup: () => mockRouteSetupResult,
|
|
97
|
+
}));
|
|
98
|
+
|
|
75
99
|
mock.module("../config/env.js", () => ({
|
|
76
100
|
isHttpAuthDisabled: () => true,
|
|
77
101
|
getGatewayInternalBaseUrl: () => "http://gateway.internal:7830",
|
|
@@ -112,6 +136,34 @@ const mockConfigObj = {
|
|
|
112
136
|
elevenlabs: {},
|
|
113
137
|
},
|
|
114
138
|
},
|
|
139
|
+
services: {
|
|
140
|
+
stt: {
|
|
141
|
+
mode: "your-own" as const,
|
|
142
|
+
provider: "deepgram" as const,
|
|
143
|
+
providers: {},
|
|
144
|
+
},
|
|
145
|
+
tts: {
|
|
146
|
+
mode: "your-own" as const,
|
|
147
|
+
provider: "elevenlabs" as const,
|
|
148
|
+
providers: {
|
|
149
|
+
elevenlabs: {
|
|
150
|
+
voiceId: DEFAULT_ELEVENLABS_VOICE_ID,
|
|
151
|
+
voiceModelId: "",
|
|
152
|
+
speed: 1.0,
|
|
153
|
+
stability: 0.5,
|
|
154
|
+
similarityBoost: 0.75,
|
|
155
|
+
conversationTimeoutSeconds: 30,
|
|
156
|
+
},
|
|
157
|
+
"fish-audio": {
|
|
158
|
+
referenceId: "",
|
|
159
|
+
chunkLength: 200,
|
|
160
|
+
format: "mp3" as const,
|
|
161
|
+
latency: "normal" as const,
|
|
162
|
+
speed: 1.0,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
115
167
|
};
|
|
116
168
|
|
|
117
169
|
mock.module("../config/loader.js", () => ({
|
|
@@ -243,6 +295,11 @@ mock.module("../inbound/public-ingress-urls.js", () => ({
|
|
|
243
295
|
const wsBase = base.replace(/^http(s?)/, "ws$1");
|
|
244
296
|
return `${wsBase}/webhooks/twilio/relay`;
|
|
245
297
|
},
|
|
298
|
+
getTwilioMediaStreamUrl: (ingressConfig: unknown) => {
|
|
299
|
+
const base = resolveIngressBaseUrlFromConfig(ingressConfig);
|
|
300
|
+
const wsBase = base.replace(/^http(s?)/, "ws$1");
|
|
301
|
+
return `${wsBase}/webhooks/twilio/media-stream`;
|
|
302
|
+
},
|
|
246
303
|
getTwilioVoiceWebhookUrl: (ingressConfig: unknown) =>
|
|
247
304
|
`${resolveIngressBaseUrlFromConfig(ingressConfig)}/webhooks/twilio/voice`,
|
|
248
305
|
getTwilioStatusCallbackUrl: (ingressConfig: unknown) =>
|
|
@@ -398,6 +455,18 @@ describe("twilio webhook routes", () => {
|
|
|
398
455
|
updatePhoneNumberWebhookCalls = [];
|
|
399
456
|
mockTwilioApiValidationStatus = 200;
|
|
400
457
|
mockTwilioApiValidationBody = JSON.stringify({ sid: "AC_validated" });
|
|
458
|
+
// Reset STT config to defaults between tests
|
|
459
|
+
mockConfigObj.services.stt.provider = "deepgram" as any;
|
|
460
|
+
// Reset routeSetup mock to default normal_call
|
|
461
|
+
mockRouteSetupResult = {
|
|
462
|
+
outcome: { action: "normal_call", isInbound: true },
|
|
463
|
+
resolved: {
|
|
464
|
+
assistantId: "self",
|
|
465
|
+
isInbound: true,
|
|
466
|
+
otherPartyNumber: "+15559998888",
|
|
467
|
+
actorTrust: { trustClass: "guardian", memberRecord: null },
|
|
468
|
+
},
|
|
469
|
+
};
|
|
401
470
|
|
|
402
471
|
globalThis.fetch = (async (
|
|
403
472
|
url: string | URL | Request,
|
|
@@ -1006,6 +1075,313 @@ describe("twilio webhook routes", () => {
|
|
|
1006
1075
|
});
|
|
1007
1076
|
});
|
|
1008
1077
|
|
|
1078
|
+
// ── services.stt-driven TwiML routing ───────────────────────────────
|
|
1079
|
+
// These tests assert that the voice webhook TwiML path is determined
|
|
1080
|
+
// by services.stt.provider via resolveTelephonySttRouting — the
|
|
1081
|
+
// canonical telephony STT routing resolver.
|
|
1082
|
+
|
|
1083
|
+
describe("services.stt-driven TwiML routing", () => {
|
|
1084
|
+
test("outbound: deepgram -> ConversationRelay with transcriptionProvider=Deepgram", async () => {
|
|
1085
|
+
mockConfigObj.services.stt.provider = "deepgram" as any;
|
|
1086
|
+
const session = createTestSession("conv-stt-dg-1", "CA_stt_dg_1");
|
|
1087
|
+
const req = makeVoiceRequest(session.id, { CallSid: "CA_stt_dg_1" });
|
|
1088
|
+
|
|
1089
|
+
const res = await handleVoiceWebhook(req);
|
|
1090
|
+
expect(res.status).toBe(200);
|
|
1091
|
+
|
|
1092
|
+
const twiml = await res.text();
|
|
1093
|
+
expect(twiml).toContain("<ConversationRelay");
|
|
1094
|
+
expect(twiml).not.toContain("<Stream");
|
|
1095
|
+
expect(twiml).toContain('transcriptionProvider="Deepgram"');
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test("inbound: deepgram -> ConversationRelay with transcriptionProvider=Deepgram", async () => {
|
|
1099
|
+
mockConfigObj.services.stt.provider = "deepgram" as any;
|
|
1100
|
+
const req = makeInboundVoiceRequest({
|
|
1101
|
+
CallSid: "CA_stt_dg_inbound_1",
|
|
1102
|
+
From: "+14155551234",
|
|
1103
|
+
To: "+15550001111",
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
const res = await handleVoiceWebhook(req);
|
|
1107
|
+
expect(res.status).toBe(200);
|
|
1108
|
+
|
|
1109
|
+
const twiml = await res.text();
|
|
1110
|
+
expect(twiml).toContain("<ConversationRelay");
|
|
1111
|
+
expect(twiml).toContain('transcriptionProvider="Deepgram"');
|
|
1112
|
+
expect(twiml).not.toContain("<Stream");
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
test("outbound: google-gemini -> ConversationRelay with transcriptionProvider=Google", async () => {
|
|
1116
|
+
mockConfigObj.services.stt.provider = "google-gemini" as any;
|
|
1117
|
+
const session = createTestSession("conv-stt-gg-1", "CA_stt_gg_1");
|
|
1118
|
+
const req = makeVoiceRequest(session.id, { CallSid: "CA_stt_gg_1" });
|
|
1119
|
+
|
|
1120
|
+
const res = await handleVoiceWebhook(req);
|
|
1121
|
+
expect(res.status).toBe(200);
|
|
1122
|
+
|
|
1123
|
+
const twiml = await res.text();
|
|
1124
|
+
expect(twiml).toContain("<ConversationRelay");
|
|
1125
|
+
expect(twiml).not.toContain("<Stream");
|
|
1126
|
+
expect(twiml).toContain('transcriptionProvider="Google"');
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
test("outbound: openai-whisper -> Stream TwiML with path-based metadata (no ConversationRelay)", async () => {
|
|
1130
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1131
|
+
const session = createTestSession("conv-stt-ow-1", "CA_stt_ow_1");
|
|
1132
|
+
const req = makeVoiceRequest(session.id, { CallSid: "CA_stt_ow_1" });
|
|
1133
|
+
|
|
1134
|
+
const res = await handleVoiceWebhook(req);
|
|
1135
|
+
expect(res.status).toBe(200);
|
|
1136
|
+
|
|
1137
|
+
const twiml = await res.text();
|
|
1138
|
+
expect(twiml).toContain("<Stream");
|
|
1139
|
+
expect(twiml).not.toContain("<ConversationRelay");
|
|
1140
|
+
expect(twiml).not.toContain("transcriptionProvider=");
|
|
1141
|
+
// callSessionId is in the URL path, not as a query param
|
|
1142
|
+
expect(twiml).toContain(
|
|
1143
|
+
`wss://ingress.example.com/webhooks/twilio/media-stream/${session.id}`,
|
|
1144
|
+
);
|
|
1145
|
+
expect(twiml).not.toContain("?callSessionId=");
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
test("inbound: openai-whisper -> Stream TwiML (no ConversationRelay)", async () => {
|
|
1149
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1150
|
+
const req = makeInboundVoiceRequest({
|
|
1151
|
+
CallSid: "CA_stt_ow_inbound_1",
|
|
1152
|
+
From: "+14155551234",
|
|
1153
|
+
To: "+15550001111",
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
const res = await handleVoiceWebhook(req);
|
|
1157
|
+
expect(res.status).toBe(200);
|
|
1158
|
+
|
|
1159
|
+
const twiml = await res.text();
|
|
1160
|
+
expect(twiml).toContain("<Stream");
|
|
1161
|
+
expect(twiml).not.toContain("<ConversationRelay");
|
|
1162
|
+
expect(twiml).not.toContain("transcriptionProvider=");
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
test("Stream TwiML includes auth token as Parameter", async () => {
|
|
1166
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1167
|
+
const session = createTestSession(
|
|
1168
|
+
"conv-stt-ow-token-1",
|
|
1169
|
+
"CA_stt_ow_token_1",
|
|
1170
|
+
);
|
|
1171
|
+
const req = makeVoiceRequest(session.id, {
|
|
1172
|
+
CallSid: "CA_stt_ow_token_1",
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
const res = await handleVoiceWebhook(req);
|
|
1176
|
+
expect(res.status).toBe(200);
|
|
1177
|
+
|
|
1178
|
+
const twiml = await res.text();
|
|
1179
|
+
expect(twiml).toContain('<Parameter name="token"');
|
|
1180
|
+
expect(twiml).toContain('<Parameter name="callSessionId"');
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test("routing is driven exclusively by services.stt.provider", async () => {
|
|
1184
|
+
// Telephony STT routing reads services.stt.provider only.
|
|
1185
|
+
// calls.voice.transcriptionProvider is a legacy config field that
|
|
1186
|
+
// no longer participates in the call setup path.
|
|
1187
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1188
|
+
const session = createTestSession(
|
|
1189
|
+
"conv-stt-routing-1",
|
|
1190
|
+
"CA_stt_routing_1",
|
|
1191
|
+
);
|
|
1192
|
+
const req = makeVoiceRequest(session.id, { CallSid: "CA_stt_routing_1" });
|
|
1193
|
+
|
|
1194
|
+
const res = await handleVoiceWebhook(req);
|
|
1195
|
+
expect(res.status).toBe(200);
|
|
1196
|
+
|
|
1197
|
+
const twiml = await res.text();
|
|
1198
|
+
// Must be Stream (from services.stt), NOT ConversationRelay
|
|
1199
|
+
expect(twiml).toContain("<Stream");
|
|
1200
|
+
expect(twiml).not.toContain("<ConversationRelay");
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// ── Media-stream preflight setup guardrails ──────────────────────────
|
|
1205
|
+
// These tests assert that the TwiML preflight guard in
|
|
1206
|
+
// buildVoiceWebhookTwiml rejects unsupported interactive setup actions
|
|
1207
|
+
// for media-stream-custom calls before stream bootstrap, falling back
|
|
1208
|
+
// to ConversationRelay for interactive flows.
|
|
1209
|
+
|
|
1210
|
+
describe("media-stream preflight setup guardrails", () => {
|
|
1211
|
+
test("media-stream: normal_call setup proceeds with Stream TwiML", async () => {
|
|
1212
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1213
|
+
mockRouteSetupResult = {
|
|
1214
|
+
outcome: { action: "normal_call", isInbound: true },
|
|
1215
|
+
resolved: {
|
|
1216
|
+
assistantId: "self",
|
|
1217
|
+
isInbound: true,
|
|
1218
|
+
otherPartyNumber: "+15559998888",
|
|
1219
|
+
actorTrust: { trustClass: "guardian", memberRecord: null },
|
|
1220
|
+
},
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
const session = createTestSession("conv-ms-normal-1", "CA_ms_normal_1");
|
|
1224
|
+
const req = makeVoiceRequest(session.id, { CallSid: "CA_ms_normal_1" });
|
|
1225
|
+
|
|
1226
|
+
const res = await handleVoiceWebhook(req);
|
|
1227
|
+
expect(res.status).toBe(200);
|
|
1228
|
+
|
|
1229
|
+
const twiml = await res.text();
|
|
1230
|
+
expect(twiml).toContain("<Stream");
|
|
1231
|
+
expect(twiml).not.toContain("<ConversationRelay");
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
test("media-stream: deny setup proceeds with Stream TwiML (deny is handled at stream level)", async () => {
|
|
1235
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1236
|
+
mockRouteSetupResult = {
|
|
1237
|
+
outcome: {
|
|
1238
|
+
action: "deny",
|
|
1239
|
+
message: "This number is not authorized.",
|
|
1240
|
+
logReason: "Inbound voice ACL: blocked caller",
|
|
1241
|
+
},
|
|
1242
|
+
resolved: {
|
|
1243
|
+
assistantId: "self",
|
|
1244
|
+
isInbound: true,
|
|
1245
|
+
otherPartyNumber: "+15559998888",
|
|
1246
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
1247
|
+
},
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const session = createTestSession("conv-ms-deny-1", "CA_ms_deny_1");
|
|
1251
|
+
const req = makeVoiceRequest(session.id, { CallSid: "CA_ms_deny_1" });
|
|
1252
|
+
|
|
1253
|
+
const res = await handleVoiceWebhook(req);
|
|
1254
|
+
expect(res.status).toBe(200);
|
|
1255
|
+
|
|
1256
|
+
const twiml = await res.text();
|
|
1257
|
+
// Deny is supported on media-stream — should still produce Stream TwiML
|
|
1258
|
+
expect(twiml).toContain("<Stream");
|
|
1259
|
+
expect(twiml).not.toContain("<ConversationRelay");
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
test("media-stream: verification setup falls back to ConversationRelay", async () => {
|
|
1263
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1264
|
+
mockRouteSetupResult = {
|
|
1265
|
+
outcome: {
|
|
1266
|
+
action: "verification",
|
|
1267
|
+
assistantId: "self",
|
|
1268
|
+
fromNumber: "+14155551234",
|
|
1269
|
+
},
|
|
1270
|
+
resolved: {
|
|
1271
|
+
assistantId: "self",
|
|
1272
|
+
isInbound: true,
|
|
1273
|
+
otherPartyNumber: "+14155551234",
|
|
1274
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
1275
|
+
},
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const session = createTestSession("conv-ms-verify-1", "CA_ms_verify_1");
|
|
1279
|
+
const req = makeVoiceRequest(session.id, { CallSid: "CA_ms_verify_1" });
|
|
1280
|
+
|
|
1281
|
+
const res = await handleVoiceWebhook(req);
|
|
1282
|
+
expect(res.status).toBe(200);
|
|
1283
|
+
|
|
1284
|
+
const twiml = await res.text();
|
|
1285
|
+
// Interactive verification cannot work on media-stream — must fall back
|
|
1286
|
+
expect(twiml).toContain("<ConversationRelay");
|
|
1287
|
+
expect(twiml).not.toContain("<Stream");
|
|
1288
|
+
expect(twiml).toContain('transcriptionProvider="Deepgram"');
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
test("media-stream: name_capture setup falls back to ConversationRelay", async () => {
|
|
1292
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1293
|
+
mockRouteSetupResult = {
|
|
1294
|
+
outcome: {
|
|
1295
|
+
action: "name_capture",
|
|
1296
|
+
assistantId: "self",
|
|
1297
|
+
fromNumber: "+14155551234",
|
|
1298
|
+
},
|
|
1299
|
+
resolved: {
|
|
1300
|
+
assistantId: "self",
|
|
1301
|
+
isInbound: true,
|
|
1302
|
+
otherPartyNumber: "+14155551234",
|
|
1303
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const session = createTestSession("conv-ms-namecap-1", "CA_ms_namecap_1");
|
|
1308
|
+
const req = makeVoiceRequest(session.id, {
|
|
1309
|
+
CallSid: "CA_ms_namecap_1",
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
const res = await handleVoiceWebhook(req);
|
|
1313
|
+
expect(res.status).toBe(200);
|
|
1314
|
+
|
|
1315
|
+
const twiml = await res.text();
|
|
1316
|
+
expect(twiml).toContain("<ConversationRelay");
|
|
1317
|
+
expect(twiml).not.toContain("<Stream");
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
test("media-stream: invite_redemption setup falls back to ConversationRelay", async () => {
|
|
1321
|
+
mockConfigObj.services.stt.provider = "openai-whisper" as any;
|
|
1322
|
+
mockRouteSetupResult = {
|
|
1323
|
+
outcome: {
|
|
1324
|
+
action: "invite_redemption",
|
|
1325
|
+
assistantId: "self",
|
|
1326
|
+
fromNumber: "+14155551234",
|
|
1327
|
+
friendName: "Alice",
|
|
1328
|
+
guardianName: "Bob",
|
|
1329
|
+
},
|
|
1330
|
+
resolved: {
|
|
1331
|
+
assistantId: "self",
|
|
1332
|
+
isInbound: true,
|
|
1333
|
+
otherPartyNumber: "+14155551234",
|
|
1334
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const session = createTestSession("conv-ms-invite-1", "CA_ms_invite_1");
|
|
1339
|
+
const req = makeVoiceRequest(session.id, {
|
|
1340
|
+
CallSid: "CA_ms_invite_1",
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
const res = await handleVoiceWebhook(req);
|
|
1344
|
+
expect(res.status).toBe(200);
|
|
1345
|
+
|
|
1346
|
+
const twiml = await res.text();
|
|
1347
|
+
expect(twiml).toContain("<ConversationRelay");
|
|
1348
|
+
expect(twiml).not.toContain("<Stream");
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
test("conversation-relay-native: unsupported setup does not trigger preflight (not media-stream)", async () => {
|
|
1352
|
+
// When the STT provider is deepgram (conversation-relay-native),
|
|
1353
|
+
// the preflight guard is not invoked — setup flows are handled
|
|
1354
|
+
// natively by the relay server.
|
|
1355
|
+
mockConfigObj.services.stt.provider = "deepgram" as any;
|
|
1356
|
+
mockRouteSetupResult = {
|
|
1357
|
+
outcome: {
|
|
1358
|
+
action: "verification",
|
|
1359
|
+
assistantId: "self",
|
|
1360
|
+
fromNumber: "+14155551234",
|
|
1361
|
+
},
|
|
1362
|
+
resolved: {
|
|
1363
|
+
assistantId: "self",
|
|
1364
|
+
isInbound: true,
|
|
1365
|
+
otherPartyNumber: "+14155551234",
|
|
1366
|
+
actorTrust: { trustClass: "unknown", memberRecord: null },
|
|
1367
|
+
},
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
const session = createTestSession("conv-cr-verify-1", "CA_cr_verify_1");
|
|
1371
|
+
const req = makeVoiceRequest(session.id, {
|
|
1372
|
+
CallSid: "CA_cr_verify_1",
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
const res = await handleVoiceWebhook(req);
|
|
1376
|
+
expect(res.status).toBe(200);
|
|
1377
|
+
|
|
1378
|
+
const twiml = await res.text();
|
|
1379
|
+
// ConversationRelay should be emitted regardless of setup outcome
|
|
1380
|
+
expect(twiml).toContain("<ConversationRelay");
|
|
1381
|
+
expect(twiml).not.toContain("<Stream");
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1009
1385
|
describe("Twilio control-plane credential and number operations", () => {
|
|
1010
1386
|
test("setting credentials stores them and returns success", async () => {
|
|
1011
1387
|
mockRawConfigStore = {};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
safeStringSlice,
|
|
5
|
+
stripOrphanedSurrogates,
|
|
6
|
+
stripOrphanedSurrogatesDeep,
|
|
7
|
+
} from "../util/unicode.js";
|
|
8
|
+
|
|
9
|
+
// U+1F389 PARTY POPPER = "\uD83C\uDF89" (a surrogate pair).
|
|
10
|
+
const EMOJI = "\uD83C\uDF89";
|
|
11
|
+
const HIGH = "\uD83C";
|
|
12
|
+
const LOW = "\uDF89";
|
|
13
|
+
const REPLACEMENT = "\uFFFD";
|
|
14
|
+
|
|
15
|
+
describe("safeStringSlice", () => {
|
|
16
|
+
test("no-op when string has no surrogates", () => {
|
|
17
|
+
expect(safeStringSlice("hello world", 0, 5)).toBe("hello");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("behaves like slice when no surrogates on the boundary", () => {
|
|
21
|
+
const s = `abc${EMOJI}xyz`;
|
|
22
|
+
// slice at position 0-3: "abc", no boundary trouble
|
|
23
|
+
expect(safeStringSlice(s, 0, 3)).toBe("abc");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("backs off high surrogate at end when more string follows", () => {
|
|
27
|
+
// "abc" + high surrogate at position 3, low at position 4, then "xyz".
|
|
28
|
+
// Cutting at end=4 would land between the pair — back off to 3.
|
|
29
|
+
const s = `abc${EMOJI}xyz`;
|
|
30
|
+
const result = safeStringSlice(s, 0, 4);
|
|
31
|
+
expect(result).toBe("abc");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("preserves a complete surrogate pair at end", () => {
|
|
35
|
+
const s = `abc${EMOJI}xyz`;
|
|
36
|
+
// end=5 includes both code units of the pair (positions 3 and 4).
|
|
37
|
+
const result = safeStringSlice(s, 0, 5);
|
|
38
|
+
expect(result).toBe(`abc${EMOJI}`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("does NOT repair trailing high surrogate when end === length", () => {
|
|
42
|
+
// An already-orphaned high surrogate at the tail must not be silently
|
|
43
|
+
// dropped by safeStringSlice — that's the sanitizer's job. safeStringSlice
|
|
44
|
+
// only protects against creating NEW orphans at a cut boundary.
|
|
45
|
+
const s = `abc${HIGH}`;
|
|
46
|
+
expect(safeStringSlice(s, 0, s.length)).toBe(`abc${HIGH}`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("advances start past orphaned low surrogate", () => {
|
|
50
|
+
const s = `abc${EMOJI}xyz`;
|
|
51
|
+
// Starting at position 4 would begin mid-pair (on the low surrogate) —
|
|
52
|
+
// advance to position 5.
|
|
53
|
+
const result = safeStringSlice(s, 4, s.length);
|
|
54
|
+
expect(result).toBe("xyz");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("does NOT advance when start === 0", () => {
|
|
58
|
+
// start=0 can never land mid-pair — leave leading orphans alone.
|
|
59
|
+
const s = `${LOW}abc`;
|
|
60
|
+
expect(safeStringSlice(s, 0, s.length)).toBe(`${LOW}abc`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("handles both start mid-pair and end mid-pair in one call", () => {
|
|
64
|
+
const s = `${EMOJI}abc${EMOJI}xyz`;
|
|
65
|
+
// Slice from position 1 (low surrogate) to 6 (high surrogate of second emoji).
|
|
66
|
+
// Should advance start to 2 and back off end to 5.
|
|
67
|
+
const result = safeStringSlice(s, 1, 6);
|
|
68
|
+
expect(result).toBe("abc");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("clamps start and end into range", () => {
|
|
72
|
+
expect(safeStringSlice("hello", -5, 100)).toBe("hello");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("returns empty string when start > end after adjustment", () => {
|
|
76
|
+
const s = EMOJI;
|
|
77
|
+
// start=1 (low surrogate) advances to 2. end=1 (clamped to length=2). Empty range.
|
|
78
|
+
const result = safeStringSlice(s, 1, 1);
|
|
79
|
+
expect(result).toBe("");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("stripOrphanedSurrogates", () => {
|
|
84
|
+
test("returns same reference for ASCII", () => {
|
|
85
|
+
const s = "hello world";
|
|
86
|
+
expect(stripOrphanedSurrogates(s)).toBe(s);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("returns same reference for BMP-only text", () => {
|
|
90
|
+
const s = "héllo wörld";
|
|
91
|
+
expect(stripOrphanedSurrogates(s)).toBe(s);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("returns same reference when all surrogates are paired", () => {
|
|
95
|
+
const s = `hello ${EMOJI} world ${EMOJI}`;
|
|
96
|
+
expect(stripOrphanedSurrogates(s)).toBe(s);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("replaces a lone high surrogate with U+FFFD", () => {
|
|
100
|
+
const s = `abc${HIGH}xyz`;
|
|
101
|
+
expect(stripOrphanedSurrogates(s)).toBe(`abc${REPLACEMENT}xyz`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("replaces a lone low surrogate with U+FFFD", () => {
|
|
105
|
+
const s = `abc${LOW}xyz`;
|
|
106
|
+
expect(stripOrphanedSurrogates(s)).toBe(`abc${REPLACEMENT}xyz`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("replaces a lone high surrogate at the very end", () => {
|
|
110
|
+
const s = `abc${HIGH}`;
|
|
111
|
+
expect(stripOrphanedSurrogates(s)).toBe(`abc${REPLACEMENT}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("replaces a lone low surrogate at the very start", () => {
|
|
115
|
+
const s = `${LOW}abc`;
|
|
116
|
+
expect(stripOrphanedSurrogates(s)).toBe(`${REPLACEMENT}abc`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("preserves valid pairs while replacing orphans", () => {
|
|
120
|
+
const s = `${EMOJI}${HIGH}${EMOJI}`;
|
|
121
|
+
expect(stripOrphanedSurrogates(s)).toBe(`${EMOJI}${REPLACEMENT}${EMOJI}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("handles two high surrogates in a row (both orphans)", () => {
|
|
125
|
+
const s = `${HIGH}${HIGH}xyz`;
|
|
126
|
+
expect(stripOrphanedSurrogates(s)).toBe(
|
|
127
|
+
`${REPLACEMENT}${REPLACEMENT}xyz`,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("produces output that round-trips through JSON", () => {
|
|
132
|
+
const s = `abc${HIGH}xyz`;
|
|
133
|
+
const cleaned = stripOrphanedSurrogates(s);
|
|
134
|
+
expect(() => JSON.stringify(cleaned)).not.toThrow();
|
|
135
|
+
// And the serialized string is valid JSON that parses back.
|
|
136
|
+
const json = JSON.stringify(cleaned);
|
|
137
|
+
expect(JSON.parse(json)).toBe(cleaned);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("stripOrphanedSurrogatesDeep", () => {
|
|
142
|
+
test("returns same reference on a clean string", () => {
|
|
143
|
+
const input = "hello";
|
|
144
|
+
const result = stripOrphanedSurrogatesDeep(input);
|
|
145
|
+
expect(result.changed).toBe(false);
|
|
146
|
+
expect(result.value).toBe(input);
|
|
147
|
+
expect(result.fixedStringCount).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("returns same reference on a clean object tree", () => {
|
|
151
|
+
const input = {
|
|
152
|
+
a: "hello",
|
|
153
|
+
b: { c: ["world", 42, null, { d: EMOJI }] },
|
|
154
|
+
};
|
|
155
|
+
const result = stripOrphanedSurrogatesDeep(input);
|
|
156
|
+
expect(result.changed).toBe(false);
|
|
157
|
+
expect(result.value).toBe(input);
|
|
158
|
+
expect(result.fixedStringCount).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("rewrites nested strings with orphans", () => {
|
|
162
|
+
const input: { a: string; b: Array<{ c: string } | string> } = {
|
|
163
|
+
a: "clean",
|
|
164
|
+
b: [{ c: `bad${HIGH}` }, "also clean"],
|
|
165
|
+
};
|
|
166
|
+
const result = stripOrphanedSurrogatesDeep(input);
|
|
167
|
+
expect(result.changed).toBe(true);
|
|
168
|
+
expect(result.fixedStringCount).toBe(1);
|
|
169
|
+
const firstChild = result.value.b[0] as { c: string };
|
|
170
|
+
expect(firstChild.c).toBe(`bad${REPLACEMENT}`);
|
|
171
|
+
// Clean siblings are preserved by value (structural equality).
|
|
172
|
+
expect(result.value.a).toBe("clean");
|
|
173
|
+
expect(result.value.b[1]).toBe("also clean");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("leaves non-plain objects untouched", () => {
|
|
177
|
+
class Custom {
|
|
178
|
+
value = `bad${HIGH}`;
|
|
179
|
+
}
|
|
180
|
+
const inst = new Custom();
|
|
181
|
+
const result = stripOrphanedSurrogatesDeep({ inst });
|
|
182
|
+
// We don't walk class instances — they pass through unchanged.
|
|
183
|
+
expect(result.changed).toBe(false);
|
|
184
|
+
expect(result.value.inst).toBe(inst);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("counts multiple fixed strings", () => {
|
|
188
|
+
const input = {
|
|
189
|
+
a: `one${HIGH}`,
|
|
190
|
+
b: `two${LOW}`,
|
|
191
|
+
c: "clean",
|
|
192
|
+
};
|
|
193
|
+
const result = stripOrphanedSurrogatesDeep(input);
|
|
194
|
+
expect(result.changed).toBe(true);
|
|
195
|
+
expect(result.fixedStringCount).toBe(2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("preserves reference identity of every clean container in a nested tree", () => {
|
|
199
|
+
// The happy path must not allocate shadow arrays/objects — this runs on
|
|
200
|
+
// every outbound Anthropic request, so GC churn adds up. Verify that the
|
|
201
|
+
// top-level object AND every nested container is returned by reference.
|
|
202
|
+
const innerArr = ["a", "b", EMOJI];
|
|
203
|
+
const innerObj = { x: 1, y: "ok", z: innerArr };
|
|
204
|
+
const input = {
|
|
205
|
+
a: "hello",
|
|
206
|
+
b: innerObj,
|
|
207
|
+
c: [innerObj, "clean", EMOJI],
|
|
208
|
+
};
|
|
209
|
+
const result = stripOrphanedSurrogatesDeep(input);
|
|
210
|
+
expect(result.changed).toBe(false);
|
|
211
|
+
expect(result.value).toBe(input);
|
|
212
|
+
expect(result.value.b).toBe(innerObj);
|
|
213
|
+
expect(result.value.b.z).toBe(innerArr);
|
|
214
|
+
expect(result.value.c).toBe(input.c);
|
|
215
|
+
expect(result.value.c[0]).toBe(innerObj);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("clean siblings alongside a dirty child reuse original references where possible", () => {
|
|
219
|
+
// When one branch changes, only the containers on the path from root to
|
|
220
|
+
// the change should be reallocated — untouched sibling subtrees must keep
|
|
221
|
+
// their original reference.
|
|
222
|
+
const cleanBranch = { deep: { list: ["a", "b"] } };
|
|
223
|
+
const input = {
|
|
224
|
+
clean: cleanBranch,
|
|
225
|
+
dirty: { value: `bad${HIGH}` },
|
|
226
|
+
};
|
|
227
|
+
const result = stripOrphanedSurrogatesDeep(input);
|
|
228
|
+
expect(result.changed).toBe(true);
|
|
229
|
+
expect(result.value).not.toBe(input);
|
|
230
|
+
expect(result.value.clean).toBe(cleanBranch);
|
|
231
|
+
expect(result.value.clean.deep).toBe(cleanBranch.deep);
|
|
232
|
+
expect(result.value.clean.deep.list).toBe(cleanBranch.deep.list);
|
|
233
|
+
expect(result.value.dirty).not.toBe(input.dirty);
|
|
234
|
+
expect(result.value.dirty.value).toBe(`bad${REPLACEMENT}`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("array with a dirty tail preserves clean leading element references", () => {
|
|
238
|
+
const cleanItem = { k: "v" };
|
|
239
|
+
const input = [cleanItem, `bad${HIGH}`];
|
|
240
|
+
const result = stripOrphanedSurrogatesDeep(input);
|
|
241
|
+
expect(result.changed).toBe(true);
|
|
242
|
+
expect(result.value).not.toBe(input);
|
|
243
|
+
expect(result.value[0]).toBe(cleanItem);
|
|
244
|
+
expect(result.value[1]).toBe(`bad${REPLACEMENT}`);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("array subclass with hostile Symbol.species still clones safely", () => {
|
|
248
|
+
// Regression: Array.prototype.slice consults ArraySpeciesCreate, so an
|
|
249
|
+
// Array subclass with a custom Symbol.species could produce a non-Array
|
|
250
|
+
// clone whose push() doesn't exist — crashing the sanitizer. The fix
|
|
251
|
+
// must build a plain Array literal instead.
|
|
252
|
+
class HostileContainer {
|
|
253
|
+
items: unknown[] = [];
|
|
254
|
+
// Intentionally no `push` method — mimics the shape ArraySpeciesCreate
|
|
255
|
+
// would produce for a subclass that returns a non-Array constructor.
|
|
256
|
+
}
|
|
257
|
+
class WeirdArray extends Array {
|
|
258
|
+
static get [Symbol.species](): ArrayConstructor {
|
|
259
|
+
return HostileContainer as unknown as ArrayConstructor;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const input = new WeirdArray();
|
|
263
|
+
input.push("clean", `bad${HIGH}`);
|
|
264
|
+
const result = stripOrphanedSurrogatesDeep(input);
|
|
265
|
+
expect(result.changed).toBe(true);
|
|
266
|
+
expect(Array.isArray(result.value)).toBe(true);
|
|
267
|
+
expect(result.value).toEqual(["clean", `bad${REPLACEMENT}`]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("rewritten output can be JSON-stringified end-to-end", () => {
|
|
271
|
+
// This is the exact shape of the bug: a payload with an orphaned high
|
|
272
|
+
// surrogate buried in a tool_result content string. After sanitization,
|
|
273
|
+
// JSON.stringify must succeed and the JSON must parse back cleanly.
|
|
274
|
+
const payload = {
|
|
275
|
+
messages: [
|
|
276
|
+
{
|
|
277
|
+
role: "user",
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "tool_result",
|
|
281
|
+
tool_use_id: "abc",
|
|
282
|
+
content: `shell output before ${HIGH} shell output after`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
const result = stripOrphanedSurrogatesDeep(payload);
|
|
289
|
+
expect(result.changed).toBe(true);
|
|
290
|
+
const json = JSON.stringify(result.value);
|
|
291
|
+
expect(() => JSON.parse(json)).not.toThrow();
|
|
292
|
+
});
|
|
293
|
+
});
|