@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
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
2
8
|
import { join } from "node:path";
|
|
3
9
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
10
|
|
|
@@ -51,8 +57,14 @@ import { invalidateConfigCache, loadConfig } from "../config/loader.js";
|
|
|
51
57
|
import {
|
|
52
58
|
AssistantConfigSchema,
|
|
53
59
|
DEFAULT_ELEVENLABS_VOICE_ID,
|
|
60
|
+
SttServiceSchema,
|
|
61
|
+
TtsServiceSchema,
|
|
62
|
+
VALID_TTS_SERVICE_PROVIDERS,
|
|
54
63
|
} from "../config/schema.js";
|
|
64
|
+
import type { AssistantConfig } from "../config/types.js";
|
|
55
65
|
import { _setStorePath } from "../security/encrypted-store.js";
|
|
66
|
+
import { listCatalogProviderIds } from "../tts/provider-catalog.js";
|
|
67
|
+
import { resolveTtsConfig } from "../tts/tts-config-resolver.js";
|
|
56
68
|
|
|
57
69
|
// ---------------------------------------------------------------------------
|
|
58
70
|
// Helpers
|
|
@@ -166,7 +178,7 @@ describe("AssistantConfigSchema", () => {
|
|
|
166
178
|
enqueueIntervalMs: 6 * 60 * 60 * 1000,
|
|
167
179
|
supersededItemRetentionMs: 30 * 24 * 60 * 60 * 1000,
|
|
168
180
|
conversationRetentionDays: 0,
|
|
169
|
-
llmRequestLogRetentionMs: 1 *
|
|
181
|
+
llmRequestLogRetentionMs: 1 * 60 * 60 * 1000,
|
|
170
182
|
});
|
|
171
183
|
});
|
|
172
184
|
|
|
@@ -177,6 +189,63 @@ describe("AssistantConfigSchema", () => {
|
|
|
177
189
|
expect(result.success).toBe(false);
|
|
178
190
|
});
|
|
179
191
|
|
|
192
|
+
test("accepts memory.cleanup.llmRequestLogRetentionMs at the 365-day boundary", () => {
|
|
193
|
+
const max = 365 * 24 * 60 * 60 * 1000;
|
|
194
|
+
const result = AssistantConfigSchema.safeParse({
|
|
195
|
+
memory: { cleanup: { llmRequestLogRetentionMs: max } },
|
|
196
|
+
});
|
|
197
|
+
expect(result.success).toBe(true);
|
|
198
|
+
if (result.success) {
|
|
199
|
+
expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBe(max);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("rejects memory.cleanup.llmRequestLogRetentionMs above 365 days", () => {
|
|
204
|
+
// This must match the gateway's MAX_LLM_REQUEST_LOG_RETENTION_MS. Without
|
|
205
|
+
// the Zod .max(), a manually edited config.json with a large value would
|
|
206
|
+
// be silently accepted by the daemon and then truncated by the macOS
|
|
207
|
+
// picker on the next PATCH — a quiet data-loss bug.
|
|
208
|
+
const overMax = 365 * 24 * 60 * 60 * 1000 + 1;
|
|
209
|
+
const result = AssistantConfigSchema.safeParse({
|
|
210
|
+
memory: { cleanup: { llmRequestLogRetentionMs: overMax } },
|
|
211
|
+
});
|
|
212
|
+
expect(result.success).toBe(false);
|
|
213
|
+
if (!result.success) {
|
|
214
|
+
expect(
|
|
215
|
+
result.error.issues.some((i) =>
|
|
216
|
+
i.path.includes("llmRequestLogRetentionMs"),
|
|
217
|
+
),
|
|
218
|
+
).toBe(true);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("rejects negative memory.cleanup.llmRequestLogRetentionMs", () => {
|
|
223
|
+
const result = AssistantConfigSchema.safeParse({
|
|
224
|
+
memory: { cleanup: { llmRequestLogRetentionMs: -1 } },
|
|
225
|
+
});
|
|
226
|
+
expect(result.success).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("accepts null memory.cleanup.llmRequestLogRetentionMs (keep forever)", () => {
|
|
230
|
+
const result = AssistantConfigSchema.safeParse({
|
|
231
|
+
memory: { cleanup: { llmRequestLogRetentionMs: null } },
|
|
232
|
+
});
|
|
233
|
+
expect(result.success).toBe(true);
|
|
234
|
+
if (result.success) {
|
|
235
|
+
expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBeNull();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("accepts memory.cleanup.llmRequestLogRetentionMs: 0 (prune immediately)", () => {
|
|
240
|
+
const result = AssistantConfigSchema.safeParse({
|
|
241
|
+
memory: { cleanup: { llmRequestLogRetentionMs: 0 } },
|
|
242
|
+
});
|
|
243
|
+
expect(result.success).toBe(true);
|
|
244
|
+
if (result.success) {
|
|
245
|
+
expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBe(0);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
180
249
|
test("rejects invalid provider", () => {
|
|
181
250
|
const result = AssistantConfigSchema.safeParse({
|
|
182
251
|
services: { inference: { provider: "invalid" } },
|
|
@@ -602,8 +671,6 @@ describe("AssistantConfigSchema", () => {
|
|
|
602
671
|
},
|
|
603
672
|
voice: {
|
|
604
673
|
language: "en-US",
|
|
605
|
-
transcriptionProvider: "Deepgram",
|
|
606
|
-
ttsProvider: "elevenlabs",
|
|
607
674
|
hints: [],
|
|
608
675
|
interruptSensitivity: "low",
|
|
609
676
|
},
|
|
@@ -711,29 +778,8 @@ describe("AssistantConfigSchema", () => {
|
|
|
711
778
|
test("config without calls.voice parses correctly and produces defaults", () => {
|
|
712
779
|
const result = AssistantConfigSchema.parse({});
|
|
713
780
|
expect(result.calls.voice.language).toBe("en-US");
|
|
714
|
-
expect(result.calls.voice.
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
test("elevenlabs tuning params have correct defaults", () => {
|
|
718
|
-
const result = AssistantConfigSchema.parse({});
|
|
719
|
-
expect(result.elevenlabs.voiceModelId).toBe("");
|
|
720
|
-
expect(result.elevenlabs.speed).toBe(1.0);
|
|
721
|
-
expect(result.elevenlabs.stability).toBe(0.5);
|
|
722
|
-
expect(result.elevenlabs.similarityBoost).toBe(0.75);
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
test("rejects elevenlabs.speed below 0.7", () => {
|
|
726
|
-
const result = AssistantConfigSchema.safeParse({
|
|
727
|
-
elevenlabs: { speed: 0.5 },
|
|
728
|
-
});
|
|
729
|
-
expect(result.success).toBe(false);
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
test("rejects elevenlabs.speed above 1.2", () => {
|
|
733
|
-
const result = AssistantConfigSchema.safeParse({
|
|
734
|
-
elevenlabs: { speed: 1.5 },
|
|
735
|
-
});
|
|
736
|
-
expect(result.success).toBe(false);
|
|
781
|
+
expect(result.calls.voice.hints).toEqual([]);
|
|
782
|
+
expect(result.calls.voice.interruptSensitivity).toBe("low");
|
|
737
783
|
});
|
|
738
784
|
|
|
739
785
|
test("accepts valid calls.voice overrides", () => {
|
|
@@ -741,39 +787,20 @@ describe("AssistantConfigSchema", () => {
|
|
|
741
787
|
calls: {
|
|
742
788
|
voice: {
|
|
743
789
|
language: "es-ES",
|
|
744
|
-
transcriptionProvider: "Google",
|
|
745
790
|
},
|
|
746
791
|
},
|
|
747
|
-
elevenlabs: {
|
|
748
|
-
stability: 0.8,
|
|
749
|
-
},
|
|
750
792
|
});
|
|
751
793
|
expect(result.calls.voice.language).toBe("es-ES");
|
|
752
|
-
expect(result.calls.voice.transcriptionProvider).toBe("Google");
|
|
753
|
-
expect(result.elevenlabs.stability).toBe(0.8);
|
|
754
|
-
// Defaults preserved for unset fields
|
|
755
|
-
expect(result.elevenlabs.voiceModelId).toBe("");
|
|
756
|
-
expect(result.elevenlabs.similarityBoost).toBe(0.75);
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
test("rejects invalid calls.voice.transcriptionProvider", () => {
|
|
760
|
-
const result = AssistantConfigSchema.safeParse({
|
|
761
|
-
calls: { voice: { transcriptionProvider: "AWS" } },
|
|
762
|
-
});
|
|
763
|
-
expect(result.success).toBe(false);
|
|
764
|
-
if (!result.success) {
|
|
765
|
-
const msgs = result.error.issues.map((i) => i.message);
|
|
766
|
-
expect(
|
|
767
|
-
msgs.some((m) => m.includes("calls.voice.transcriptionProvider")),
|
|
768
|
-
).toBe(true);
|
|
769
|
-
}
|
|
770
794
|
});
|
|
771
795
|
|
|
772
|
-
test("
|
|
773
|
-
|
|
774
|
-
|
|
796
|
+
test("transcriptionProvider is no longer part of the voice config schema", () => {
|
|
797
|
+
// Zod strips unrecognized keys by default — the legacy field is silently ignored.
|
|
798
|
+
const result = AssistantConfigSchema.parse({
|
|
799
|
+
calls: { voice: { transcriptionProvider: "Google" } },
|
|
775
800
|
});
|
|
776
|
-
expect(
|
|
801
|
+
expect(
|
|
802
|
+
(result.calls.voice as Record<string, unknown>).transcriptionProvider,
|
|
803
|
+
).toBeUndefined();
|
|
777
804
|
});
|
|
778
805
|
|
|
779
806
|
test("accepts optional calls.model", () => {
|
|
@@ -850,6 +877,10 @@ describe("AssistantConfigSchema", () => {
|
|
|
850
877
|
host: "localhost",
|
|
851
878
|
port: 9222,
|
|
852
879
|
probeTimeoutMs: 500,
|
|
880
|
+
desktopAuto: {
|
|
881
|
+
enabled: true,
|
|
882
|
+
cooldownMs: 30_000,
|
|
883
|
+
},
|
|
853
884
|
},
|
|
854
885
|
});
|
|
855
886
|
});
|
|
@@ -958,6 +989,410 @@ describe("AssistantConfigSchema", () => {
|
|
|
958
989
|
});
|
|
959
990
|
expect(result.success).toBe(false);
|
|
960
991
|
});
|
|
992
|
+
|
|
993
|
+
// ── services.tts config ──────────────────────────────────────────────
|
|
994
|
+
|
|
995
|
+
test("applies services.tts defaults when not specified", () => {
|
|
996
|
+
const result = AssistantConfigSchema.parse({});
|
|
997
|
+
expect(result.services.tts.mode).toBe("your-own");
|
|
998
|
+
expect(result.services.tts.provider).toBe("elevenlabs");
|
|
999
|
+
expect(result.services.tts.providers.elevenlabs.voiceId).toBe(
|
|
1000
|
+
DEFAULT_ELEVENLABS_VOICE_ID,
|
|
1001
|
+
);
|
|
1002
|
+
expect(result.services.tts.providers.elevenlabs.speed).toBe(1.0);
|
|
1003
|
+
expect(result.services.tts.providers.elevenlabs.stability).toBe(0.5);
|
|
1004
|
+
expect(result.services.tts.providers.elevenlabs.similarityBoost).toBe(0.75);
|
|
1005
|
+
expect(
|
|
1006
|
+
result.services.tts.providers.elevenlabs.conversationTimeoutSeconds,
|
|
1007
|
+
).toBe(30);
|
|
1008
|
+
expect(result.services.tts.providers["fish-audio"].referenceId).toBe("");
|
|
1009
|
+
expect(result.services.tts.providers["fish-audio"].chunkLength).toBe(200);
|
|
1010
|
+
expect(result.services.tts.providers["fish-audio"].format).toBe("mp3");
|
|
1011
|
+
expect(result.services.tts.providers["fish-audio"].speed).toBe(1.0);
|
|
1012
|
+
expect(result.services.tts.providers.deepgram.model).toBe(
|
|
1013
|
+
"aura-asteria-en",
|
|
1014
|
+
);
|
|
1015
|
+
expect(result.services.tts.providers.deepgram.format).toBe("mp3");
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
test("accepts valid services.tts provider override", () => {
|
|
1019
|
+
const result = AssistantConfigSchema.parse({
|
|
1020
|
+
services: { tts: { provider: "fish-audio" } },
|
|
1021
|
+
});
|
|
1022
|
+
expect(result.services.tts.provider).toBe("fish-audio");
|
|
1023
|
+
expect(result.services.tts.mode).toBe("your-own");
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test("accepts deepgram as services.tts.provider", () => {
|
|
1027
|
+
const result = AssistantConfigSchema.parse({
|
|
1028
|
+
services: { tts: { provider: "deepgram" } },
|
|
1029
|
+
});
|
|
1030
|
+
expect(result.services.tts.provider).toBe("deepgram");
|
|
1031
|
+
expect(result.services.tts.mode).toBe("your-own");
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
test("accepts valid services.tts.providers.elevenlabs overrides", () => {
|
|
1035
|
+
const result = AssistantConfigSchema.parse({
|
|
1036
|
+
services: {
|
|
1037
|
+
tts: {
|
|
1038
|
+
providers: {
|
|
1039
|
+
elevenlabs: { voiceId: "custom-voice", speed: 0.8 },
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
expect(result.services.tts.providers.elevenlabs.voiceId).toBe(
|
|
1045
|
+
"custom-voice",
|
|
1046
|
+
);
|
|
1047
|
+
expect(result.services.tts.providers.elevenlabs.speed).toBe(0.8);
|
|
1048
|
+
// Unset fields preserve defaults
|
|
1049
|
+
expect(result.services.tts.providers.elevenlabs.stability).toBe(0.5);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
test("accepts valid services.tts.providers.fish-audio overrides", () => {
|
|
1053
|
+
const result = AssistantConfigSchema.parse({
|
|
1054
|
+
services: {
|
|
1055
|
+
tts: {
|
|
1056
|
+
providers: {
|
|
1057
|
+
"fish-audio": { referenceId: "my-voice", format: "wav" },
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
expect(result.services.tts.providers["fish-audio"].referenceId).toBe(
|
|
1063
|
+
"my-voice",
|
|
1064
|
+
);
|
|
1065
|
+
expect(result.services.tts.providers["fish-audio"].format).toBe("wav");
|
|
1066
|
+
// Defaults preserved
|
|
1067
|
+
expect(result.services.tts.providers["fish-audio"].chunkLength).toBe(200);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
test("accepts valid services.tts.providers.deepgram overrides", () => {
|
|
1071
|
+
const result = AssistantConfigSchema.parse({
|
|
1072
|
+
services: {
|
|
1073
|
+
tts: {
|
|
1074
|
+
providers: {
|
|
1075
|
+
deepgram: { model: "aura-luna-en", format: "opus" },
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
expect(result.services.tts.providers.deepgram.model).toBe("aura-luna-en");
|
|
1081
|
+
expect(result.services.tts.providers.deepgram.format).toBe("opus");
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
test("rejects services.tts.mode = managed", () => {
|
|
1085
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1086
|
+
services: { tts: { mode: "managed" } },
|
|
1087
|
+
});
|
|
1088
|
+
expect(result.success).toBe(false);
|
|
1089
|
+
if (!result.success) {
|
|
1090
|
+
const msgs = result.error.issues.map((i) => i.message);
|
|
1091
|
+
expect(
|
|
1092
|
+
msgs.some((m) => m.includes("your-own") || m.includes("managed")),
|
|
1093
|
+
).toBe(true);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// ── hostBrowser.cdpInspect.desktopAuto config ───────────────────────
|
|
1098
|
+
|
|
1099
|
+
test("applies hostBrowser.cdpInspect.desktopAuto defaults", () => {
|
|
1100
|
+
const result = AssistantConfigSchema.parse({});
|
|
1101
|
+
expect(result.hostBrowser.cdpInspect.desktopAuto).toEqual({
|
|
1102
|
+
enabled: true,
|
|
1103
|
+
cooldownMs: 30_000,
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
test("accepts hostBrowser.cdpInspect.desktopAuto overrides", () => {
|
|
1108
|
+
const result = AssistantConfigSchema.parse({
|
|
1109
|
+
hostBrowser: {
|
|
1110
|
+
cdpInspect: {
|
|
1111
|
+
desktopAuto: { enabled: false, cooldownMs: 10_000 },
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
});
|
|
1115
|
+
expect(result.hostBrowser.cdpInspect.desktopAuto.enabled).toBe(false);
|
|
1116
|
+
expect(result.hostBrowser.cdpInspect.desktopAuto.cooldownMs).toBe(10_000);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
test("accepts hostBrowser.cdpInspect.desktopAuto.cooldownMs of 0 (disable cooldown)", () => {
|
|
1120
|
+
const result = AssistantConfigSchema.parse({
|
|
1121
|
+
hostBrowser: {
|
|
1122
|
+
cdpInspect: { desktopAuto: { cooldownMs: 0 } },
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
expect(result.hostBrowser.cdpInspect.desktopAuto.cooldownMs).toBe(0);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test("rejects hostBrowser.cdpInspect.desktopAuto.cooldownMs below 0", () => {
|
|
1129
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1130
|
+
hostBrowser: {
|
|
1131
|
+
cdpInspect: { desktopAuto: { cooldownMs: -1 } },
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
expect(result.success).toBe(false);
|
|
1135
|
+
if (!result.success) {
|
|
1136
|
+
expect(
|
|
1137
|
+
result.error.issues.some((issue) =>
|
|
1138
|
+
issue.path.join(".").includes("cooldownMs"),
|
|
1139
|
+
),
|
|
1140
|
+
).toBe(true);
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
test("rejects invalid services.tts.provider", () => {
|
|
1145
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1146
|
+
services: { tts: { provider: "aws-polly" } },
|
|
1147
|
+
});
|
|
1148
|
+
expect(result.success).toBe(false);
|
|
1149
|
+
if (!result.success) {
|
|
1150
|
+
const msgs = result.error.issues.map((i) => i.message);
|
|
1151
|
+
expect(msgs.some((m) => m.includes("services.tts.provider"))).toBe(true);
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
test("services.tts.mode only accepts your-own as literal", () => {
|
|
1156
|
+
// Explicit your-own should work
|
|
1157
|
+
const valid = TtsServiceSchema.safeParse({ mode: "your-own" });
|
|
1158
|
+
expect(valid.success).toBe(true);
|
|
1159
|
+
|
|
1160
|
+
// managed should be rejected
|
|
1161
|
+
const invalid = TtsServiceSchema.safeParse({ mode: "managed" });
|
|
1162
|
+
expect(invalid.success).toBe(false);
|
|
1163
|
+
|
|
1164
|
+
// Any other string should be rejected
|
|
1165
|
+
const invalid2 = TtsServiceSchema.safeParse({ mode: "self-hosted" });
|
|
1166
|
+
expect(invalid2.success).toBe(false);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
// ── services.stt config ──────────────────────────────────────────────
|
|
1170
|
+
|
|
1171
|
+
test("rejects services.stt without explicit provider", () => {
|
|
1172
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1173
|
+
services: { stt: { mode: "your-own" } },
|
|
1174
|
+
});
|
|
1175
|
+
expect(result.success).toBe(false);
|
|
1176
|
+
if (!result.success) {
|
|
1177
|
+
expect(
|
|
1178
|
+
result.error.issues.some((i) => i.path.join(".").includes("provider")),
|
|
1179
|
+
).toBe(true);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test("applies services.stt structural defaults when provider is explicit", () => {
|
|
1184
|
+
const result = AssistantConfigSchema.parse({
|
|
1185
|
+
services: { stt: { provider: "openai-whisper" } },
|
|
1186
|
+
});
|
|
1187
|
+
expect(result.services.stt.mode).toBe("your-own");
|
|
1188
|
+
expect(result.services.stt.provider).toBe("openai-whisper");
|
|
1189
|
+
// providers defaults to empty sparse map
|
|
1190
|
+
expect(result.services.stt.providers).toEqual({});
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
test("accepts valid services.stt provider override", () => {
|
|
1194
|
+
const result = AssistantConfigSchema.parse({
|
|
1195
|
+
services: { stt: { provider: "openai-whisper" } },
|
|
1196
|
+
});
|
|
1197
|
+
expect(result.services.stt.provider).toBe("openai-whisper");
|
|
1198
|
+
expect(result.services.stt.mode).toBe("your-own");
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
test("accepts valid services.stt.providers.openai-whisper overrides", () => {
|
|
1202
|
+
const result = AssistantConfigSchema.parse({
|
|
1203
|
+
services: {
|
|
1204
|
+
stt: {
|
|
1205
|
+
provider: "openai-whisper",
|
|
1206
|
+
providers: {
|
|
1207
|
+
"openai-whisper": {},
|
|
1208
|
+
},
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
expect(result.services.stt.providers["openai-whisper"]).toEqual({});
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
test("parses when providers map is empty (sparse default)", () => {
|
|
1216
|
+
const result = AssistantConfigSchema.parse({
|
|
1217
|
+
services: { stt: { provider: "deepgram", providers: {} } },
|
|
1218
|
+
});
|
|
1219
|
+
expect(result.services.stt.providers).toEqual({});
|
|
1220
|
+
expect(result.services.stt.provider).toBe("deepgram");
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
test("parses when unknown future provider blobs exist under providers", () => {
|
|
1224
|
+
const result = AssistantConfigSchema.parse({
|
|
1225
|
+
services: {
|
|
1226
|
+
stt: {
|
|
1227
|
+
provider: "openai-whisper",
|
|
1228
|
+
providers: {
|
|
1229
|
+
"openai-whisper": {},
|
|
1230
|
+
"future-provider": { model: "next-gen", lang: "en" },
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
expect(result.services.stt.providers["openai-whisper"]).toEqual({});
|
|
1236
|
+
expect(result.services.stt.providers["future-provider"]).toEqual({
|
|
1237
|
+
model: "next-gen",
|
|
1238
|
+
lang: "en",
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
test("rejects services.stt.mode = managed", () => {
|
|
1243
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1244
|
+
services: { stt: { mode: "managed" } },
|
|
1245
|
+
});
|
|
1246
|
+
expect(result.success).toBe(false);
|
|
1247
|
+
if (!result.success) {
|
|
1248
|
+
const msgs = result.error.issues.map((i) => i.message);
|
|
1249
|
+
expect(
|
|
1250
|
+
msgs.some((m) => m.includes("your-own") || m.includes("managed")),
|
|
1251
|
+
).toBe(true);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test("rejects invalid services.stt.provider", () => {
|
|
1256
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1257
|
+
services: { stt: { provider: "azure-speech" } },
|
|
1258
|
+
});
|
|
1259
|
+
expect(result.success).toBe(false);
|
|
1260
|
+
if (!result.success) {
|
|
1261
|
+
const msgs = result.error.issues.map((i) => i.message);
|
|
1262
|
+
expect(msgs.some((m) => m.includes("services.stt.provider"))).toBe(true);
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
test("accepts deepgram as services.stt.provider", () => {
|
|
1267
|
+
const result = AssistantConfigSchema.parse({
|
|
1268
|
+
services: { stt: { provider: "deepgram" } },
|
|
1269
|
+
});
|
|
1270
|
+
expect(result.services.stt.provider).toBe("deepgram");
|
|
1271
|
+
expect(result.services.stt.mode).toBe("your-own");
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
test("accepts google-gemini as services.stt.provider", () => {
|
|
1275
|
+
const result = AssistantConfigSchema.parse({
|
|
1276
|
+
services: { stt: { provider: "google-gemini" } },
|
|
1277
|
+
});
|
|
1278
|
+
expect(result.services.stt.provider).toBe("google-gemini");
|
|
1279
|
+
expect(result.services.stt.mode).toBe("your-own");
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test("applies services.stt structural defaults when google-gemini provider is explicit", () => {
|
|
1283
|
+
const result = AssistantConfigSchema.parse({
|
|
1284
|
+
services: { stt: { provider: "google-gemini" } },
|
|
1285
|
+
});
|
|
1286
|
+
expect(result.services.stt.mode).toBe("your-own");
|
|
1287
|
+
expect(result.services.stt.provider).toBe("google-gemini");
|
|
1288
|
+
expect(result.services.stt.providers).toEqual({});
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
test("accepts valid services.stt.providers.deepgram overrides", () => {
|
|
1292
|
+
const result = AssistantConfigSchema.parse({
|
|
1293
|
+
services: {
|
|
1294
|
+
stt: {
|
|
1295
|
+
provider: "deepgram",
|
|
1296
|
+
providers: {
|
|
1297
|
+
deepgram: {},
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
});
|
|
1302
|
+
expect(result.services.stt.providers.deepgram).toEqual({});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
test("existing configs with explicit per-provider objects continue to parse", () => {
|
|
1306
|
+
// Configs with explicit per-provider objects must continue to
|
|
1307
|
+
// parse and round-trip successfully.
|
|
1308
|
+
const result = AssistantConfigSchema.parse({
|
|
1309
|
+
services: {
|
|
1310
|
+
stt: {
|
|
1311
|
+
provider: "openai-whisper",
|
|
1312
|
+
providers: {
|
|
1313
|
+
"openai-whisper": {},
|
|
1314
|
+
deepgram: {},
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
},
|
|
1318
|
+
});
|
|
1319
|
+
expect(result.services.stt.providers["openai-whisper"]).toEqual({});
|
|
1320
|
+
expect(result.services.stt.providers.deepgram).toEqual({});
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
test("services.stt.provider is required (no implicit default)", () => {
|
|
1324
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1325
|
+
services: { stt: {} },
|
|
1326
|
+
});
|
|
1327
|
+
expect(result.success).toBe(false);
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
test("services.stt.mode only accepts your-own as literal", () => {
|
|
1331
|
+
// Explicit your-own should work
|
|
1332
|
+
const valid = SttServiceSchema.safeParse({
|
|
1333
|
+
mode: "your-own",
|
|
1334
|
+
provider: "openai-whisper",
|
|
1335
|
+
});
|
|
1336
|
+
expect(valid.success).toBe(true);
|
|
1337
|
+
|
|
1338
|
+
// managed should be rejected
|
|
1339
|
+
const invalid = SttServiceSchema.safeParse({
|
|
1340
|
+
mode: "managed",
|
|
1341
|
+
provider: "openai-whisper",
|
|
1342
|
+
});
|
|
1343
|
+
expect(invalid.success).toBe(false);
|
|
1344
|
+
|
|
1345
|
+
// Any other string should be rejected
|
|
1346
|
+
const invalid2 = SttServiceSchema.safeParse({
|
|
1347
|
+
mode: "self-hosted",
|
|
1348
|
+
provider: "openai-whisper",
|
|
1349
|
+
});
|
|
1350
|
+
expect(invalid2.success).toBe(false);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
test("rejects hostBrowser.cdpInspect.desktopAuto.cooldownMs above 300000", () => {
|
|
1354
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1355
|
+
hostBrowser: {
|
|
1356
|
+
cdpInspect: { desktopAuto: { cooldownMs: 500_000 } },
|
|
1357
|
+
},
|
|
1358
|
+
});
|
|
1359
|
+
expect(result.success).toBe(false);
|
|
1360
|
+
if (!result.success) {
|
|
1361
|
+
expect(
|
|
1362
|
+
result.error.issues.some((issue) =>
|
|
1363
|
+
issue.path.join(".").includes("cooldownMs"),
|
|
1364
|
+
),
|
|
1365
|
+
).toBe(true);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
test("rejects non-integer hostBrowser.cdpInspect.desktopAuto.cooldownMs", () => {
|
|
1370
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1371
|
+
hostBrowser: {
|
|
1372
|
+
cdpInspect: { desktopAuto: { cooldownMs: 5000.5 } },
|
|
1373
|
+
},
|
|
1374
|
+
});
|
|
1375
|
+
expect(result.success).toBe(false);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
test("rejects non-boolean hostBrowser.cdpInspect.desktopAuto.enabled", () => {
|
|
1379
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1380
|
+
hostBrowser: {
|
|
1381
|
+
cdpInspect: { desktopAuto: { enabled: "yes" } },
|
|
1382
|
+
},
|
|
1383
|
+
});
|
|
1384
|
+
expect(result.success).toBe(false);
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
test("desktopAuto defaults preserved when only cdpInspect.enabled is set", () => {
|
|
1388
|
+
const result = AssistantConfigSchema.parse({
|
|
1389
|
+
hostBrowser: { cdpInspect: { enabled: true } },
|
|
1390
|
+
});
|
|
1391
|
+
expect(result.hostBrowser.cdpInspect.desktopAuto).toEqual({
|
|
1392
|
+
enabled: true,
|
|
1393
|
+
cooldownMs: 30_000,
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
961
1396
|
});
|
|
962
1397
|
|
|
963
1398
|
// ---------------------------------------------------------------------------
|
|
@@ -969,12 +1404,15 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
969
1404
|
const config = AssistantConfigSchema.parse({});
|
|
970
1405
|
const profile = resolveVoiceQualityProfile(config);
|
|
971
1406
|
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
972
|
-
expect(profile.transcriptionProvider).toBe("Deepgram");
|
|
973
1407
|
});
|
|
974
1408
|
|
|
975
|
-
test("uses
|
|
1409
|
+
test("uses services.tts.providers.elevenlabs.voiceId for voice", () => {
|
|
976
1410
|
const config = AssistantConfigSchema.parse({
|
|
977
|
-
|
|
1411
|
+
services: {
|
|
1412
|
+
tts: {
|
|
1413
|
+
providers: { elevenlabs: { voiceId: "test-voice-id" } },
|
|
1414
|
+
},
|
|
1415
|
+
},
|
|
978
1416
|
});
|
|
979
1417
|
const profile = resolveVoiceQualityProfile(config);
|
|
980
1418
|
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
@@ -987,14 +1425,20 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
987
1425
|
expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
|
|
988
1426
|
});
|
|
989
1427
|
|
|
990
|
-
test("applies voice tuning params from elevenlabs config", () => {
|
|
1428
|
+
test("applies voice tuning params from services.tts.providers.elevenlabs config", () => {
|
|
991
1429
|
const config = AssistantConfigSchema.parse({
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1430
|
+
services: {
|
|
1431
|
+
tts: {
|
|
1432
|
+
providers: {
|
|
1433
|
+
elevenlabs: {
|
|
1434
|
+
voiceId: "abc123",
|
|
1435
|
+
voiceModelId: "turbo_v2_5",
|
|
1436
|
+
speed: 0.9,
|
|
1437
|
+
stability: 0.8,
|
|
1438
|
+
similarityBoost: 0.9,
|
|
1439
|
+
},
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
998
1442
|
},
|
|
999
1443
|
});
|
|
1000
1444
|
const profile = resolveVoiceQualityProfile(config);
|
|
@@ -1042,13 +1486,424 @@ describe("buildElevenLabsVoiceSpec", () => {
|
|
|
1042
1486
|
|
|
1043
1487
|
test("default config uses a bare voiceId when no model override is set", () => {
|
|
1044
1488
|
const config = AssistantConfigSchema.parse({
|
|
1045
|
-
|
|
1489
|
+
services: {
|
|
1490
|
+
tts: {
|
|
1491
|
+
providers: { elevenlabs: { voiceId: "test" } },
|
|
1492
|
+
},
|
|
1493
|
+
},
|
|
1046
1494
|
});
|
|
1047
|
-
const spec = buildElevenLabsVoiceSpec(
|
|
1495
|
+
const spec = buildElevenLabsVoiceSpec(
|
|
1496
|
+
config.services.tts.providers.elevenlabs,
|
|
1497
|
+
);
|
|
1048
1498
|
expect(spec).toBe("test");
|
|
1049
1499
|
});
|
|
1050
1500
|
});
|
|
1051
1501
|
|
|
1502
|
+
// ---------------------------------------------------------------------------
|
|
1503
|
+
// Tests: TTS config resolver
|
|
1504
|
+
// ---------------------------------------------------------------------------
|
|
1505
|
+
|
|
1506
|
+
describe("resolveTtsConfig", () => {
|
|
1507
|
+
test("returns default provider and config from empty config", () => {
|
|
1508
|
+
const config = AssistantConfigSchema.parse({});
|
|
1509
|
+
const resolved = resolveTtsConfig(config);
|
|
1510
|
+
expect(resolved.provider).toBe("elevenlabs");
|
|
1511
|
+
expect(resolved.providerConfig).toMatchObject({
|
|
1512
|
+
voiceId: DEFAULT_ELEVENLABS_VOICE_ID,
|
|
1513
|
+
speed: 1.0,
|
|
1514
|
+
stability: 0.5,
|
|
1515
|
+
similarityBoost: 0.75,
|
|
1516
|
+
});
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
test("uses canonical services.tts.provider when set", () => {
|
|
1520
|
+
const config = AssistantConfigSchema.parse({
|
|
1521
|
+
services: { tts: { provider: "fish-audio" } },
|
|
1522
|
+
});
|
|
1523
|
+
const resolved = resolveTtsConfig(config);
|
|
1524
|
+
expect(resolved.provider).toBe("fish-audio");
|
|
1525
|
+
expect(resolved.providerConfig).toMatchObject({
|
|
1526
|
+
referenceId: "",
|
|
1527
|
+
chunkLength: 200,
|
|
1528
|
+
format: "mp3",
|
|
1529
|
+
speed: 1.0,
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
test("returns canonical elevenlabs config from services.tts.providers", () => {
|
|
1534
|
+
const config = AssistantConfigSchema.parse({
|
|
1535
|
+
services: {
|
|
1536
|
+
tts: {
|
|
1537
|
+
provider: "elevenlabs",
|
|
1538
|
+
providers: {
|
|
1539
|
+
elevenlabs: { voiceId: "canonical-voice", stability: 0.9 },
|
|
1540
|
+
},
|
|
1541
|
+
},
|
|
1542
|
+
},
|
|
1543
|
+
});
|
|
1544
|
+
const resolved = resolveTtsConfig(config);
|
|
1545
|
+
expect(resolved.provider).toBe("elevenlabs");
|
|
1546
|
+
expect(resolved.providerConfig).toMatchObject({
|
|
1547
|
+
voiceId: "canonical-voice",
|
|
1548
|
+
stability: 0.9,
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
test("uses canonical elevenlabs config exclusively (no legacy fallback)", () => {
|
|
1553
|
+
const config = AssistantConfigSchema.parse({
|
|
1554
|
+
services: {
|
|
1555
|
+
tts: {
|
|
1556
|
+
providers: {
|
|
1557
|
+
elevenlabs: { voiceId: "canonical-voice", speed: 0.9 },
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1560
|
+
},
|
|
1561
|
+
});
|
|
1562
|
+
const resolved = resolveTtsConfig(config);
|
|
1563
|
+
expect(resolved.provider).toBe("elevenlabs");
|
|
1564
|
+
expect(resolved.providerConfig).toMatchObject({
|
|
1565
|
+
voiceId: "canonical-voice",
|
|
1566
|
+
speed: 0.9,
|
|
1567
|
+
});
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
test("uses canonical fish-audio config exclusively (no legacy fallback)", () => {
|
|
1571
|
+
const config = AssistantConfigSchema.parse({
|
|
1572
|
+
services: {
|
|
1573
|
+
tts: {
|
|
1574
|
+
provider: "fish-audio",
|
|
1575
|
+
providers: {
|
|
1576
|
+
"fish-audio": { referenceId: "canonical-ref", format: "wav" },
|
|
1577
|
+
},
|
|
1578
|
+
},
|
|
1579
|
+
},
|
|
1580
|
+
});
|
|
1581
|
+
const resolved = resolveTtsConfig(config);
|
|
1582
|
+
expect(resolved.provider).toBe("fish-audio");
|
|
1583
|
+
expect(resolved.providerConfig).toMatchObject({
|
|
1584
|
+
referenceId: "canonical-ref",
|
|
1585
|
+
format: "wav",
|
|
1586
|
+
});
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
test("returns empty config for unknown provider", () => {
|
|
1590
|
+
// Force an unknown provider via type assertion for coverage.
|
|
1591
|
+
// structuredClone prevents mutation from leaking into Zod's shared
|
|
1592
|
+
// default objects (Zod 4 stores defaults by reference).
|
|
1593
|
+
const config = structuredClone(
|
|
1594
|
+
AssistantConfigSchema.parse({}),
|
|
1595
|
+
) as AssistantConfig;
|
|
1596
|
+
(config.services.tts as { provider: string }).provider = "aws-polly";
|
|
1597
|
+
const resolved = resolveTtsConfig(config);
|
|
1598
|
+
expect(resolved.provider).toBe("aws-polly");
|
|
1599
|
+
expect(resolved.providerConfig).toEqual({});
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
test("unknown provider resolution is deterministic across repeated calls", () => {
|
|
1603
|
+
const config = structuredClone(
|
|
1604
|
+
AssistantConfigSchema.parse({}),
|
|
1605
|
+
) as AssistantConfig;
|
|
1606
|
+
(config.services.tts as { provider: string }).provider = "nonexistent";
|
|
1607
|
+
const first = resolveTtsConfig(config);
|
|
1608
|
+
const second = resolveTtsConfig(config);
|
|
1609
|
+
expect(first).toEqual(second);
|
|
1610
|
+
expect(first.providerConfig).toEqual({});
|
|
1611
|
+
});
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// ---------------------------------------------------------------------------
|
|
1615
|
+
// Tests: TTS provider catalog integration
|
|
1616
|
+
// ---------------------------------------------------------------------------
|
|
1617
|
+
|
|
1618
|
+
describe("TTS provider catalog integration", () => {
|
|
1619
|
+
test("VALID_TTS_SERVICE_PROVIDERS matches catalog provider IDs", () => {
|
|
1620
|
+
const catalogIds = listCatalogProviderIds();
|
|
1621
|
+
expect([...VALID_TTS_SERVICE_PROVIDERS]).toEqual(catalogIds);
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
test("schema accepts all catalog provider IDs as services.tts.provider", () => {
|
|
1625
|
+
for (const providerId of listCatalogProviderIds()) {
|
|
1626
|
+
const result = AssistantConfigSchema.safeParse({
|
|
1627
|
+
services: { tts: { provider: providerId } },
|
|
1628
|
+
});
|
|
1629
|
+
expect(result.success).toBe(true);
|
|
1630
|
+
if (result.success) {
|
|
1631
|
+
expect(result.data.services.tts.provider).toBe(providerId);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
test("TtsProvidersSchema has a key for every catalog provider", () => {
|
|
1637
|
+
const parsed = AssistantConfigSchema.parse({});
|
|
1638
|
+
const providerKeys = Object.keys(parsed.services.tts.providers);
|
|
1639
|
+
for (const providerId of listCatalogProviderIds()) {
|
|
1640
|
+
expect(providerKeys).toContain(providerId);
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
test("resolveTtsConfig returns correct defaults for each catalog provider", () => {
|
|
1645
|
+
for (const providerId of listCatalogProviderIds()) {
|
|
1646
|
+
const config = AssistantConfigSchema.parse({
|
|
1647
|
+
services: { tts: { provider: providerId } },
|
|
1648
|
+
});
|
|
1649
|
+
const resolved = resolveTtsConfig(config);
|
|
1650
|
+
expect(resolved.provider).toBe(providerId);
|
|
1651
|
+
// Every catalog provider should resolve to a non-empty config object
|
|
1652
|
+
expect(Object.keys(resolved.providerConfig).length).toBeGreaterThan(0);
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
test("resolveTtsConfig returns overridden values for elevenlabs", () => {
|
|
1657
|
+
const config = AssistantConfigSchema.parse({
|
|
1658
|
+
services: {
|
|
1659
|
+
tts: {
|
|
1660
|
+
provider: "elevenlabs",
|
|
1661
|
+
providers: {
|
|
1662
|
+
elevenlabs: { voiceId: "override-voice", speed: 0.7 },
|
|
1663
|
+
},
|
|
1664
|
+
},
|
|
1665
|
+
},
|
|
1666
|
+
});
|
|
1667
|
+
const resolved = resolveTtsConfig(config);
|
|
1668
|
+
expect(resolved.provider).toBe("elevenlabs");
|
|
1669
|
+
expect(resolved.providerConfig).toMatchObject({
|
|
1670
|
+
voiceId: "override-voice",
|
|
1671
|
+
speed: 0.7,
|
|
1672
|
+
// Defaults still present for unset fields
|
|
1673
|
+
stability: 0.5,
|
|
1674
|
+
similarityBoost: 0.75,
|
|
1675
|
+
});
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
test("resolveTtsConfig returns overridden values for fish-audio", () => {
|
|
1679
|
+
const config = AssistantConfigSchema.parse({
|
|
1680
|
+
services: {
|
|
1681
|
+
tts: {
|
|
1682
|
+
provider: "fish-audio",
|
|
1683
|
+
providers: {
|
|
1684
|
+
"fish-audio": {
|
|
1685
|
+
referenceId: "override-ref",
|
|
1686
|
+
format: "opus",
|
|
1687
|
+
speed: 1.5,
|
|
1688
|
+
},
|
|
1689
|
+
},
|
|
1690
|
+
},
|
|
1691
|
+
},
|
|
1692
|
+
});
|
|
1693
|
+
const resolved = resolveTtsConfig(config);
|
|
1694
|
+
expect(resolved.provider).toBe("fish-audio");
|
|
1695
|
+
expect(resolved.providerConfig).toMatchObject({
|
|
1696
|
+
referenceId: "override-ref",
|
|
1697
|
+
format: "opus",
|
|
1698
|
+
speed: 1.5,
|
|
1699
|
+
// Defaults for unset fields
|
|
1700
|
+
chunkLength: 200,
|
|
1701
|
+
});
|
|
1702
|
+
});
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
// ---------------------------------------------------------------------------
|
|
1706
|
+
// Tests: TTS migration 032
|
|
1707
|
+
// ---------------------------------------------------------------------------
|
|
1708
|
+
|
|
1709
|
+
describe("032-tts-provider-unification migration", () => {
|
|
1710
|
+
const migrationDir = join(WORKSPACE_DIR, "_mig032");
|
|
1711
|
+
|
|
1712
|
+
beforeEach(() => {
|
|
1713
|
+
if (existsSync(migrationDir)) {
|
|
1714
|
+
rmSync(migrationDir, { recursive: true, force: true });
|
|
1715
|
+
}
|
|
1716
|
+
mkdirSync(migrationDir, { recursive: true });
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
afterEach(() => {
|
|
1720
|
+
if (existsSync(migrationDir)) {
|
|
1721
|
+
rmSync(migrationDir, { recursive: true, force: true });
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
function writeMigConfig(obj: unknown): void {
|
|
1726
|
+
writeFileSync(
|
|
1727
|
+
join(migrationDir, "config.json"),
|
|
1728
|
+
JSON.stringify(obj, null, 2),
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function readMigConfig(): Record<string, unknown> {
|
|
1733
|
+
return JSON.parse(
|
|
1734
|
+
readFileSync(join(migrationDir, "config.json"), "utf-8"),
|
|
1735
|
+
) as Record<string, unknown>;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
test("backfills provider from calls.voice.ttsProvider", async () => {
|
|
1739
|
+
writeMigConfig({
|
|
1740
|
+
calls: { voice: { ttsProvider: "fish-audio" } },
|
|
1741
|
+
});
|
|
1742
|
+
const { ttsProviderUnificationMigration } =
|
|
1743
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1744
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1745
|
+
const result = readMigConfig();
|
|
1746
|
+
const tts = (result.services as Record<string, unknown>).tts as Record<
|
|
1747
|
+
string,
|
|
1748
|
+
unknown
|
|
1749
|
+
>;
|
|
1750
|
+
expect(tts.provider).toBe("fish-audio");
|
|
1751
|
+
expect(tts.mode).toBe("your-own");
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
test("backfills elevenlabs provider config from legacy keys", async () => {
|
|
1755
|
+
writeMigConfig({
|
|
1756
|
+
calls: { voice: { ttsProvider: "elevenlabs" } },
|
|
1757
|
+
elevenlabs: { voiceId: "my-voice", speed: 0.8 },
|
|
1758
|
+
});
|
|
1759
|
+
const { ttsProviderUnificationMigration } =
|
|
1760
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1761
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1762
|
+
const result = readMigConfig();
|
|
1763
|
+
const tts = (result.services as Record<string, unknown>).tts as Record<
|
|
1764
|
+
string,
|
|
1765
|
+
unknown
|
|
1766
|
+
>;
|
|
1767
|
+
const providers = tts.providers as Record<string, Record<string, unknown>>;
|
|
1768
|
+
expect(providers.elevenlabs.voiceId).toBe("my-voice");
|
|
1769
|
+
expect(providers.elevenlabs.speed).toBe(0.8);
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
test("backfills fish-audio provider config from legacy keys", async () => {
|
|
1773
|
+
writeMigConfig({
|
|
1774
|
+
calls: { voice: { ttsProvider: "fish-audio" } },
|
|
1775
|
+
fishAudio: { referenceId: "my-ref", format: "wav" },
|
|
1776
|
+
});
|
|
1777
|
+
const { ttsProviderUnificationMigration } =
|
|
1778
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1779
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1780
|
+
const result = readMigConfig();
|
|
1781
|
+
const tts = (result.services as Record<string, unknown>).tts as Record<
|
|
1782
|
+
string,
|
|
1783
|
+
unknown
|
|
1784
|
+
>;
|
|
1785
|
+
const providers = tts.providers as Record<string, Record<string, unknown>>;
|
|
1786
|
+
expect(providers["fish-audio"].referenceId).toBe("my-ref");
|
|
1787
|
+
expect(providers["fish-audio"].format).toBe("wav");
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
test("removes legacy fields after migration", async () => {
|
|
1791
|
+
writeMigConfig({
|
|
1792
|
+
calls: { voice: { ttsProvider: "elevenlabs", language: "en-US" } },
|
|
1793
|
+
elevenlabs: { voiceId: "my-voice" },
|
|
1794
|
+
});
|
|
1795
|
+
const { ttsProviderUnificationMigration } =
|
|
1796
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1797
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1798
|
+
const result = readMigConfig();
|
|
1799
|
+
// Legacy keys removed
|
|
1800
|
+
expect(
|
|
1801
|
+
(
|
|
1802
|
+
(result.calls as Record<string, unknown>).voice as Record<
|
|
1803
|
+
string,
|
|
1804
|
+
unknown
|
|
1805
|
+
>
|
|
1806
|
+
).ttsProvider,
|
|
1807
|
+
).toBeUndefined();
|
|
1808
|
+
expect(result.elevenlabs).toBeUndefined();
|
|
1809
|
+
// Other voice fields preserved
|
|
1810
|
+
expect(
|
|
1811
|
+
(
|
|
1812
|
+
(result.calls as Record<string, unknown>).voice as Record<
|
|
1813
|
+
string,
|
|
1814
|
+
unknown
|
|
1815
|
+
>
|
|
1816
|
+
).language,
|
|
1817
|
+
).toBe("en-US");
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
test("is idempotent — repeated runs produce no changes", async () => {
|
|
1821
|
+
writeMigConfig({
|
|
1822
|
+
calls: { voice: { ttsProvider: "fish-audio" } },
|
|
1823
|
+
fishAudio: { referenceId: "my-ref" },
|
|
1824
|
+
});
|
|
1825
|
+
const { ttsProviderUnificationMigration } =
|
|
1826
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1827
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1828
|
+
const afterFirst = readMigConfig();
|
|
1829
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1830
|
+
const afterSecond = readMigConfig();
|
|
1831
|
+
expect(afterSecond).toEqual(afterFirst);
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
test("does not overwrite existing services.tts.provider", async () => {
|
|
1835
|
+
writeMigConfig({
|
|
1836
|
+
services: { tts: { provider: "elevenlabs" } },
|
|
1837
|
+
calls: { voice: { ttsProvider: "fish-audio" } },
|
|
1838
|
+
});
|
|
1839
|
+
const { ttsProviderUnificationMigration } =
|
|
1840
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1841
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1842
|
+
const result = readMigConfig();
|
|
1843
|
+
const tts = (result.services as Record<string, unknown>).tts as Record<
|
|
1844
|
+
string,
|
|
1845
|
+
unknown
|
|
1846
|
+
>;
|
|
1847
|
+
// Should keep the existing canonical value, not the legacy one
|
|
1848
|
+
expect(tts.provider).toBe("elevenlabs");
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
test("does not overwrite existing canonical provider config keys", async () => {
|
|
1852
|
+
writeMigConfig({
|
|
1853
|
+
services: {
|
|
1854
|
+
tts: {
|
|
1855
|
+
providers: {
|
|
1856
|
+
elevenlabs: { voiceId: "canonical-voice" },
|
|
1857
|
+
},
|
|
1858
|
+
},
|
|
1859
|
+
},
|
|
1860
|
+
elevenlabs: { voiceId: "legacy-voice", speed: 0.8 },
|
|
1861
|
+
});
|
|
1862
|
+
const { ttsProviderUnificationMigration } =
|
|
1863
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1864
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1865
|
+
const result = readMigConfig();
|
|
1866
|
+
const tts = (result.services as Record<string, unknown>).tts as Record<
|
|
1867
|
+
string,
|
|
1868
|
+
unknown
|
|
1869
|
+
>;
|
|
1870
|
+
const providers = tts.providers as Record<string, Record<string, unknown>>;
|
|
1871
|
+
// Canonical voiceId preserved, legacy speed backfilled
|
|
1872
|
+
expect(providers.elevenlabs.voiceId).toBe("canonical-voice");
|
|
1873
|
+
expect(providers.elevenlabs.speed).toBe(0.8);
|
|
1874
|
+
// Legacy top-level key removed
|
|
1875
|
+
expect(result.elevenlabs).toBeUndefined();
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
test("skips config without any legacy TTS fields", async () => {
|
|
1879
|
+
writeMigConfig({ maxTokens: 4096 });
|
|
1880
|
+
const { ttsProviderUnificationMigration } =
|
|
1881
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1882
|
+
const before = readMigConfig();
|
|
1883
|
+
await ttsProviderUnificationMigration.run(migrationDir);
|
|
1884
|
+
const after = readMigConfig();
|
|
1885
|
+
// Should remain unchanged (no services.tts added)
|
|
1886
|
+
expect(after).toEqual(before);
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
test("down removes services.tts from config", async () => {
|
|
1890
|
+
writeMigConfig({
|
|
1891
|
+
services: {
|
|
1892
|
+
inference: { provider: "anthropic" },
|
|
1893
|
+
tts: { provider: "elevenlabs", mode: "your-own" },
|
|
1894
|
+
},
|
|
1895
|
+
});
|
|
1896
|
+
const { ttsProviderUnificationMigration } =
|
|
1897
|
+
await import("../workspace/migrations/032-tts-provider-unification.js");
|
|
1898
|
+
await ttsProviderUnificationMigration.down(migrationDir);
|
|
1899
|
+
const result = readMigConfig();
|
|
1900
|
+
const services = result.services as Record<string, unknown>;
|
|
1901
|
+
expect(services.tts).toBeUndefined();
|
|
1902
|
+
// Other services keys preserved
|
|
1903
|
+
expect(services.inference).toBeDefined();
|
|
1904
|
+
});
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1052
1907
|
// ---------------------------------------------------------------------------
|
|
1053
1908
|
// Tests: loader integration (config file -> loadConfig with fallback)
|
|
1054
1909
|
// ---------------------------------------------------------------------------
|
|
@@ -1244,6 +2099,93 @@ describe("loadConfig with schema validation", () => {
|
|
|
1244
2099
|
expect(config.calls.provider).toBe("twilio");
|
|
1245
2100
|
});
|
|
1246
2101
|
|
|
2102
|
+
test("recovers from partial filing.activeHours without wiping unrelated fields", () => {
|
|
2103
|
+
// Only activeHoursStart is set. The superRefine must emit the issue so
|
|
2104
|
+
// the loader's delete-and-retry can strip the set field; otherwise the
|
|
2105
|
+
// mismatch persists and the config falls back to full defaults (which
|
|
2106
|
+
// would reset maxTokens below to 64000).
|
|
2107
|
+
writeConfig({
|
|
2108
|
+
maxTokens: 4096,
|
|
2109
|
+
filing: { activeHoursStart: 8 },
|
|
2110
|
+
});
|
|
2111
|
+
const config = loadConfig();
|
|
2112
|
+
expect(config.maxTokens).toBe(4096);
|
|
2113
|
+
expect(config.filing.activeHoursStart).toBeNull();
|
|
2114
|
+
expect(config.filing.activeHoursEnd).toBeNull();
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
test("recovers from partial heartbeat.activeHours without wiping unrelated fields", () => {
|
|
2118
|
+
// activeHoursStart is explicitly nulled while activeHoursEnd defaults to
|
|
2119
|
+
// 22 — a mismatch. Dual-emit strips both sides; both defaults restore
|
|
2120
|
+
// (8, 22). maxTokens is unaffected.
|
|
2121
|
+
writeConfig({
|
|
2122
|
+
maxTokens: 4096,
|
|
2123
|
+
heartbeat: { activeHoursStart: null },
|
|
2124
|
+
});
|
|
2125
|
+
const config = loadConfig();
|
|
2126
|
+
expect(config.maxTokens).toBe(4096);
|
|
2127
|
+
expect(config.heartbeat.activeHoursStart).toBe(8);
|
|
2128
|
+
expect(config.heartbeat.activeHoursEnd).toBe(22);
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
test("recovers from heartbeat.activeHours null-mismatch where explicit value equals opposite default", () => {
|
|
2132
|
+
// { start: null, end: 8 } — single-emit on the null side would strip
|
|
2133
|
+
// start, the default 8 would restore it, and the equal-hours check would
|
|
2134
|
+
// fire, cascading to a full defaults reset that wipes maxTokens.
|
|
2135
|
+
// Dual-emit strips both sides in one pass.
|
|
2136
|
+
writeConfig({
|
|
2137
|
+
maxTokens: 4096,
|
|
2138
|
+
heartbeat: { activeHoursStart: null, activeHoursEnd: 8 },
|
|
2139
|
+
});
|
|
2140
|
+
const config = loadConfig();
|
|
2141
|
+
expect(config.maxTokens).toBe(4096);
|
|
2142
|
+
expect(config.heartbeat.activeHoursStart).toBe(8);
|
|
2143
|
+
expect(config.heartbeat.activeHoursEnd).toBe(22);
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
test("recovers from heartbeat.activeHours null-mismatch on the end side", () => {
|
|
2147
|
+
// { start: 22, end: null } — same cascade class as above, mirrored.
|
|
2148
|
+
writeConfig({
|
|
2149
|
+
maxTokens: 4096,
|
|
2150
|
+
heartbeat: { activeHoursStart: 22, activeHoursEnd: null },
|
|
2151
|
+
});
|
|
2152
|
+
const config = loadConfig();
|
|
2153
|
+
expect(config.maxTokens).toBe(4096);
|
|
2154
|
+
expect(config.heartbeat.activeHoursStart).toBe(8);
|
|
2155
|
+
expect(config.heartbeat.activeHoursEnd).toBe(22);
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
test("recovers from equal heartbeat.activeHours without wiping unrelated fields", () => {
|
|
2159
|
+
// { start: 22, end: 22 } — both equal to the default for end. Single-emit
|
|
2160
|
+
// on one path would strip one side, the default would recreate the
|
|
2161
|
+
// equal-hours mismatch, and the loader would fall back to full defaults,
|
|
2162
|
+
// wiping maxTokens. Dual-emit strips both sides at once.
|
|
2163
|
+
writeConfig({
|
|
2164
|
+
maxTokens: 4096,
|
|
2165
|
+
heartbeat: { activeHoursStart: 22, activeHoursEnd: 22 },
|
|
2166
|
+
});
|
|
2167
|
+
const config = loadConfig();
|
|
2168
|
+
expect(config.maxTokens).toBe(4096);
|
|
2169
|
+
expect(config.heartbeat.activeHoursStart).toBe(8);
|
|
2170
|
+
expect(config.heartbeat.activeHoursEnd).toBe(22);
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
test("recovers from equal filing.activeHours without wiping unrelated fields", () => {
|
|
2174
|
+
// activeHoursStart === activeHoursEnd is invalid (empty window). Filing's
|
|
2175
|
+
// defaults are null/null, so single-emit on one path would strip one side
|
|
2176
|
+
// and the null default would recreate a mismatch — cascading to a full
|
|
2177
|
+
// defaults reset that wipes maxTokens. Dual-emit strips both sides so
|
|
2178
|
+
// both defaults restore to null.
|
|
2179
|
+
writeConfig({
|
|
2180
|
+
maxTokens: 1234,
|
|
2181
|
+
filing: { activeHoursStart: 5, activeHoursEnd: 5 },
|
|
2182
|
+
});
|
|
2183
|
+
const config = loadConfig();
|
|
2184
|
+
expect(config.maxTokens).toBe(1234);
|
|
2185
|
+
expect(config.filing.activeHoursStart).toBeNull();
|
|
2186
|
+
expect(config.filing.activeHoursEnd).toBeNull();
|
|
2187
|
+
});
|
|
2188
|
+
|
|
1247
2189
|
test("applies calls defaults when not specified", () => {
|
|
1248
2190
|
writeConfig({});
|
|
1249
2191
|
const config = loadConfig();
|
|
@@ -1253,7 +2195,12 @@ describe("loadConfig with schema validation", () => {
|
|
|
1253
2195
|
expect(config.calls.disclosure.enabled).toBe(true);
|
|
1254
2196
|
expect(config.calls.safety.denyCategories).toEqual([]);
|
|
1255
2197
|
expect(config.calls.voice.language).toBe("en-US");
|
|
1256
|
-
expect(
|
|
2198
|
+
expect(
|
|
2199
|
+
(config.calls.voice as Record<string, unknown>).transcriptionProvider,
|
|
2200
|
+
).toBeUndefined();
|
|
2201
|
+
expect(
|
|
2202
|
+
(config.calls.voice as Record<string, unknown>).ttsProvider,
|
|
2203
|
+
).toBeUndefined();
|
|
1257
2204
|
expect(config.calls.model).toBeUndefined();
|
|
1258
2205
|
expect(config.calls.callerIdentity).toEqual({
|
|
1259
2206
|
allowPerCallOverride: true,
|