@vellumai/assistant 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +5 -13
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/openapi.yaml +982 -72
- package/package.json +4 -6
- package/scripts/generate-openapi.ts +0 -1
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +11 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-schema.test.ts +1013 -66
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +98 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +901 -60
- package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/credential-vault-unit.test.ts +379 -3
- package/src/__tests__/credentials-cli.test.ts +40 -16
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/headless-browser-interactions.test.ts +43 -0
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +176 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +5 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +112 -26
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +18 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +43 -3
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +255 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/gmail/SKILL.md +53 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +2 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +34 -10
- package/src/config/feature-flag-registry.json +46 -14
- package/src/config/loader.ts +26 -12
- package/src/config/schema.ts +35 -10
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/services.ts +44 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +45 -12
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +101 -24
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +581 -19
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +11 -1
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +94 -4
- package/src/daemon/conversation-tool-setup.ts +25 -0
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation.ts +86 -28
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +4 -1
- package/src/daemon/handlers/shared.ts +22 -0
- package/src/daemon/handlers/skills.ts +321 -77
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +122 -25
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +34 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +14 -0
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +347 -2
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +99 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/db-init.ts +6 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +27 -18
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +9 -0
- package/src/oauth/platform-connection.test.ts +98 -0
- package/src/oauth/platform-connection.ts +52 -31
- package/src/oauth/seed-providers.ts +7 -0
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +59 -18
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -61
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +9 -0
- package/src/runtime/AGENTS.md +43 -1
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +30 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/http-server.ts +395 -10
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +34 -0
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +115 -0
- package/src/runtime/routes/conversation-routes.ts +367 -146
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +40 -5
- package/src/runtime/routes/settings-routes.ts +22 -5
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +50 -0
- package/src/security/oauth2.ts +26 -4
- package/src/security/secure-keys.ts +25 -2
- package/src/security/token-manager.ts +8 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +38 -14
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/browser-execution.ts +1163 -23
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/terminal/safe-env.ts +10 -2
- package/src/tools/terminal/shell.ts +15 -4
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +17 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +54 -10
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, jest, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { MediaTurnDetector } from "../calls/media-turn-detector.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Advance fake timers by `ms` milliseconds. Uses Bun's `jest.advanceTimersByTime`.
|
|
11
|
+
*/
|
|
12
|
+
function advance(ms: number): void {
|
|
13
|
+
jest.advanceTimersByTime(ms);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Tests
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
describe("MediaTurnDetector", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.useFakeTimers();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
jest.useRealTimers();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ── Basic lifecycle ──────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
test("starts inactive", () => {
|
|
32
|
+
const detector = new MediaTurnDetector();
|
|
33
|
+
expect(detector.isActive).toBe(false);
|
|
34
|
+
detector.dispose();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("transitions to active on first chunk", () => {
|
|
38
|
+
const detector = new MediaTurnDetector();
|
|
39
|
+
detector.onMediaChunk();
|
|
40
|
+
expect(detector.isActive).toBe(true);
|
|
41
|
+
detector.dispose();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ── Silence detection ────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
test("fires onTurnEnd with 'silence' after silence threshold", () => {
|
|
47
|
+
const onTurnStart = jest.fn();
|
|
48
|
+
const onTurnEnd = jest.fn();
|
|
49
|
+
|
|
50
|
+
const detector = new MediaTurnDetector(
|
|
51
|
+
{ silenceThresholdMs: 500 },
|
|
52
|
+
{ onTurnStart, onTurnEnd },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
detector.onMediaChunk();
|
|
56
|
+
expect(onTurnStart).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(detector.isActive).toBe(true);
|
|
58
|
+
|
|
59
|
+
// Advance past the silence threshold
|
|
60
|
+
advance(600);
|
|
61
|
+
|
|
62
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(onTurnEnd).toHaveBeenCalledWith("silence", expect.any(Number));
|
|
64
|
+
expect(detector.isActive).toBe(false);
|
|
65
|
+
|
|
66
|
+
detector.dispose();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("resets silence timer on subsequent chunks", () => {
|
|
70
|
+
const onTurnEnd = jest.fn();
|
|
71
|
+
|
|
72
|
+
const detector = new MediaTurnDetector(
|
|
73
|
+
{ silenceThresholdMs: 500 },
|
|
74
|
+
{ onTurnEnd },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
detector.onMediaChunk();
|
|
78
|
+
|
|
79
|
+
// 300ms in — silence timer has NOT fired yet
|
|
80
|
+
advance(300);
|
|
81
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
82
|
+
|
|
83
|
+
// New chunk resets the 500ms silence timer
|
|
84
|
+
detector.onMediaChunk();
|
|
85
|
+
|
|
86
|
+
// Another 300ms — still within the reset window
|
|
87
|
+
advance(300);
|
|
88
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
89
|
+
|
|
90
|
+
// 250ms more (550ms since last chunk) — past threshold
|
|
91
|
+
advance(250);
|
|
92
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(onTurnEnd).toHaveBeenCalledWith("silence", expect.any(Number));
|
|
94
|
+
|
|
95
|
+
detector.dispose();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── Max duration ─────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
test("fires onTurnEnd with 'max-duration' when hard cap is reached", () => {
|
|
101
|
+
const onTurnEnd = jest.fn();
|
|
102
|
+
|
|
103
|
+
const detector = new MediaTurnDetector(
|
|
104
|
+
{ silenceThresholdMs: 500, maxTurnDurationMs: 2000 },
|
|
105
|
+
{ onTurnEnd },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
detector.onMediaChunk();
|
|
109
|
+
|
|
110
|
+
// Keep feeding chunks so the silence timer never fires
|
|
111
|
+
for (let i = 0; i < 8; i++) {
|
|
112
|
+
advance(200);
|
|
113
|
+
detector.onMediaChunk();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// At 1600ms, still active. Advance to 2000ms.
|
|
117
|
+
advance(400);
|
|
118
|
+
|
|
119
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(onTurnEnd).toHaveBeenCalledWith("max-duration", expect.any(Number));
|
|
121
|
+
expect(detector.isActive).toBe(false);
|
|
122
|
+
|
|
123
|
+
detector.dispose();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── Turn restart ─────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
test("can start a new turn after silence ends the previous one", () => {
|
|
129
|
+
const onTurnStart = jest.fn();
|
|
130
|
+
const onTurnEnd = jest.fn();
|
|
131
|
+
|
|
132
|
+
const detector = new MediaTurnDetector(
|
|
133
|
+
{ silenceThresholdMs: 500 },
|
|
134
|
+
{ onTurnStart, onTurnEnd },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// First turn
|
|
138
|
+
detector.onMediaChunk();
|
|
139
|
+
expect(onTurnStart).toHaveBeenCalledTimes(1);
|
|
140
|
+
advance(600);
|
|
141
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(detector.isActive).toBe(false);
|
|
143
|
+
|
|
144
|
+
// Second turn
|
|
145
|
+
detector.onMediaChunk();
|
|
146
|
+
expect(onTurnStart).toHaveBeenCalledTimes(2);
|
|
147
|
+
expect(detector.isActive).toBe(true);
|
|
148
|
+
|
|
149
|
+
advance(600);
|
|
150
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(2);
|
|
151
|
+
|
|
152
|
+
detector.dispose();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── forceEnd ─────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
test("forceEnd ends the current turn immediately", () => {
|
|
158
|
+
const onTurnEnd = jest.fn();
|
|
159
|
+
|
|
160
|
+
const detector = new MediaTurnDetector(
|
|
161
|
+
{ silenceThresholdMs: 500 },
|
|
162
|
+
{ onTurnEnd },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
detector.onMediaChunk();
|
|
166
|
+
expect(detector.isActive).toBe(true);
|
|
167
|
+
|
|
168
|
+
detector.forceEnd();
|
|
169
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(onTurnEnd).toHaveBeenCalledWith("silence", expect.any(Number));
|
|
171
|
+
expect(detector.isActive).toBe(false);
|
|
172
|
+
|
|
173
|
+
detector.dispose();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("forceEnd is a no-op when inactive", () => {
|
|
177
|
+
const onTurnEnd = jest.fn();
|
|
178
|
+
|
|
179
|
+
const detector = new MediaTurnDetector(
|
|
180
|
+
{ silenceThresholdMs: 500 },
|
|
181
|
+
{ onTurnEnd },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
detector.forceEnd();
|
|
185
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
186
|
+
expect(detector.isActive).toBe(false);
|
|
187
|
+
|
|
188
|
+
detector.dispose();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── dispose ──────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
test("dispose prevents further callbacks", () => {
|
|
194
|
+
const onTurnStart = jest.fn();
|
|
195
|
+
const onTurnEnd = jest.fn();
|
|
196
|
+
|
|
197
|
+
const detector = new MediaTurnDetector(
|
|
198
|
+
{ silenceThresholdMs: 500 },
|
|
199
|
+
{ onTurnStart, onTurnEnd },
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
detector.onMediaChunk();
|
|
203
|
+
expect(onTurnStart).toHaveBeenCalledTimes(1);
|
|
204
|
+
|
|
205
|
+
detector.dispose();
|
|
206
|
+
|
|
207
|
+
// Silence timer should have been cleared — advancing should not
|
|
208
|
+
// trigger onTurnEnd.
|
|
209
|
+
advance(1000);
|
|
210
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
211
|
+
|
|
212
|
+
// Further chunks should be ignored.
|
|
213
|
+
detector.onMediaChunk();
|
|
214
|
+
expect(onTurnStart).toHaveBeenCalledTimes(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("dispose + forceEnd is a no-op", () => {
|
|
218
|
+
const onTurnEnd = jest.fn();
|
|
219
|
+
|
|
220
|
+
const detector = new MediaTurnDetector(
|
|
221
|
+
{ silenceThresholdMs: 500 },
|
|
222
|
+
{ onTurnEnd },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
detector.onMediaChunk();
|
|
226
|
+
detector.dispose();
|
|
227
|
+
detector.forceEnd();
|
|
228
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── Default config ───────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
test("uses default thresholds when config is omitted", () => {
|
|
234
|
+
const onTurnEnd = jest.fn();
|
|
235
|
+
const detector = new MediaTurnDetector({}, { onTurnEnd });
|
|
236
|
+
|
|
237
|
+
detector.onMediaChunk();
|
|
238
|
+
|
|
239
|
+
// Default silence threshold is 800ms
|
|
240
|
+
advance(700);
|
|
241
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
242
|
+
advance(200);
|
|
243
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
244
|
+
|
|
245
|
+
detector.dispose();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ── onTurnStart only fires once per turn ─────────────────────────
|
|
249
|
+
|
|
250
|
+
test("onTurnStart fires only once even with many chunks", () => {
|
|
251
|
+
const onTurnStart = jest.fn();
|
|
252
|
+
const detector = new MediaTurnDetector(
|
|
253
|
+
{ silenceThresholdMs: 500 },
|
|
254
|
+
{ onTurnStart },
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
detector.onMediaChunk();
|
|
258
|
+
detector.onMediaChunk();
|
|
259
|
+
detector.onMediaChunk();
|
|
260
|
+
detector.onMediaChunk();
|
|
261
|
+
|
|
262
|
+
expect(onTurnStart).toHaveBeenCalledTimes(1);
|
|
263
|
+
|
|
264
|
+
detector.dispose();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ── Speech-aware segmentation ─────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
describe("speech-aware segmentation", () => {
|
|
270
|
+
test("continuous chunk flow with speech->silence transition ends the turn", () => {
|
|
271
|
+
const onTurnStart = jest.fn();
|
|
272
|
+
const onTurnEnd = jest.fn();
|
|
273
|
+
|
|
274
|
+
const detector = new MediaTurnDetector(
|
|
275
|
+
{ silenceThresholdMs: 500 },
|
|
276
|
+
{ onTurnStart, onTurnEnd },
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Speech chunks — resets silence timer on each
|
|
280
|
+
detector.onMediaChunk(true);
|
|
281
|
+
expect(onTurnStart).toHaveBeenCalledTimes(1);
|
|
282
|
+
|
|
283
|
+
advance(100);
|
|
284
|
+
detector.onMediaChunk(true);
|
|
285
|
+
advance(100);
|
|
286
|
+
detector.onMediaChunk(true);
|
|
287
|
+
|
|
288
|
+
// Transition to silence — continuous silent chunks should NOT
|
|
289
|
+
// reset the silence timer, so the turn ends after the threshold.
|
|
290
|
+
advance(100);
|
|
291
|
+
detector.onMediaChunk(false);
|
|
292
|
+
advance(100);
|
|
293
|
+
detector.onMediaChunk(false);
|
|
294
|
+
advance(100);
|
|
295
|
+
detector.onMediaChunk(false);
|
|
296
|
+
|
|
297
|
+
// Not yet past threshold from last speech chunk (300ms of silence
|
|
298
|
+
// plus whatever the timer started at when silence began)
|
|
299
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
300
|
+
|
|
301
|
+
// Advance past the silence threshold from the last speech chunk
|
|
302
|
+
advance(500);
|
|
303
|
+
|
|
304
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
305
|
+
expect(onTurnEnd).toHaveBeenCalledWith("silence", expect.any(Number));
|
|
306
|
+
expect(detector.isActive).toBe(false);
|
|
307
|
+
|
|
308
|
+
detector.dispose();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("no-speech continuous noise/silence does not start a turn", () => {
|
|
312
|
+
const onTurnStart = jest.fn();
|
|
313
|
+
const onTurnEnd = jest.fn();
|
|
314
|
+
|
|
315
|
+
const detector = new MediaTurnDetector(
|
|
316
|
+
{ silenceThresholdMs: 500 },
|
|
317
|
+
{ onTurnStart, onTurnEnd },
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Send many chunks with no speech — turn should never start
|
|
321
|
+
for (let i = 0; i < 20; i++) {
|
|
322
|
+
detector.onMediaChunk(false);
|
|
323
|
+
advance(50);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
expect(onTurnStart).not.toHaveBeenCalled();
|
|
327
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
328
|
+
expect(detector.isActive).toBe(false);
|
|
329
|
+
|
|
330
|
+
detector.dispose();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("max-duration fallback still fires with continuous speech", () => {
|
|
334
|
+
const onTurnEnd = jest.fn();
|
|
335
|
+
|
|
336
|
+
const detector = new MediaTurnDetector(
|
|
337
|
+
{ silenceThresholdMs: 500, maxTurnDurationMs: 2000 },
|
|
338
|
+
{ onTurnEnd },
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Continuous speech chunks — silence timer keeps resetting
|
|
342
|
+
detector.onMediaChunk(true);
|
|
343
|
+
for (let i = 0; i < 10; i++) {
|
|
344
|
+
advance(180);
|
|
345
|
+
detector.onMediaChunk(true);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// At ~1800ms. Advance to 2000ms to hit max-duration.
|
|
349
|
+
advance(200);
|
|
350
|
+
|
|
351
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
352
|
+
expect(onTurnEnd).toHaveBeenCalledWith(
|
|
353
|
+
"max-duration",
|
|
354
|
+
expect.any(Number),
|
|
355
|
+
);
|
|
356
|
+
expect(detector.isActive).toBe(false);
|
|
357
|
+
|
|
358
|
+
detector.dispose();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("silent chunks during active turn do not reset the silence timer", () => {
|
|
362
|
+
const onTurnEnd = jest.fn();
|
|
363
|
+
|
|
364
|
+
const detector = new MediaTurnDetector(
|
|
365
|
+
{ silenceThresholdMs: 500 },
|
|
366
|
+
{ onTurnEnd },
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Start with speech
|
|
370
|
+
detector.onMediaChunk(true);
|
|
371
|
+
|
|
372
|
+
// Advance 200ms, then send silent chunks — they should NOT
|
|
373
|
+
// extend the turn by resetting the timer.
|
|
374
|
+
advance(200);
|
|
375
|
+
detector.onMediaChunk(false);
|
|
376
|
+
advance(100);
|
|
377
|
+
detector.onMediaChunk(false);
|
|
378
|
+
advance(100);
|
|
379
|
+
detector.onMediaChunk(false);
|
|
380
|
+
|
|
381
|
+
// Silence timer started from the last speech chunk. At 500ms
|
|
382
|
+
// from that point, the turn should end.
|
|
383
|
+
advance(200); // 200+100+100+200 = 600ms since last speech
|
|
384
|
+
|
|
385
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
386
|
+
expect(onTurnEnd).toHaveBeenCalledWith("silence", expect.any(Number));
|
|
387
|
+
|
|
388
|
+
detector.dispose();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("speech resuming during silence countdown resets the timer", () => {
|
|
392
|
+
const onTurnEnd = jest.fn();
|
|
393
|
+
|
|
394
|
+
const detector = new MediaTurnDetector(
|
|
395
|
+
{ silenceThresholdMs: 500 },
|
|
396
|
+
{ onTurnEnd },
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// Speech starts
|
|
400
|
+
detector.onMediaChunk(true);
|
|
401
|
+
|
|
402
|
+
// 300ms of silence
|
|
403
|
+
advance(300);
|
|
404
|
+
detector.onMediaChunk(false);
|
|
405
|
+
|
|
406
|
+
// Speech resumes — resets silence timer
|
|
407
|
+
advance(100);
|
|
408
|
+
detector.onMediaChunk(true);
|
|
409
|
+
|
|
410
|
+
// Timer reset: another 500ms must pass
|
|
411
|
+
advance(400);
|
|
412
|
+
expect(onTurnEnd).not.toHaveBeenCalled();
|
|
413
|
+
|
|
414
|
+
advance(200);
|
|
415
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
416
|
+
|
|
417
|
+
detector.dispose();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("backwards compatibility: onMediaChunk without hasSpeech argument defaults to true", () => {
|
|
421
|
+
const onTurnStart = jest.fn();
|
|
422
|
+
const onTurnEnd = jest.fn();
|
|
423
|
+
|
|
424
|
+
const detector = new MediaTurnDetector(
|
|
425
|
+
{ silenceThresholdMs: 500 },
|
|
426
|
+
{ onTurnStart, onTurnEnd },
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Call without argument — defaults to hasSpeech=true
|
|
430
|
+
detector.onMediaChunk();
|
|
431
|
+
expect(onTurnStart).toHaveBeenCalledTimes(1);
|
|
432
|
+
expect(detector.isActive).toBe(true);
|
|
433
|
+
|
|
434
|
+
advance(600);
|
|
435
|
+
expect(onTurnEnd).toHaveBeenCalledTimes(1);
|
|
436
|
+
|
|
437
|
+
detector.dispose();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_MAX_QUEUE_BYTES,
|
|
5
|
+
MessageQueue,
|
|
6
|
+
type QueuedMessage,
|
|
7
|
+
} from "../daemon/conversation-queue-manager.js";
|
|
8
|
+
|
|
9
|
+
function makeItem(content: string, requestId = "r"): QueuedMessage {
|
|
10
|
+
return {
|
|
11
|
+
content,
|
|
12
|
+
attachments: [],
|
|
13
|
+
requestId,
|
|
14
|
+
onEvent: () => {},
|
|
15
|
+
sentAt: Date.now(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("MessageQueue.peek", () => {
|
|
20
|
+
test("peek(0) on empty queue returns undefined", () => {
|
|
21
|
+
const q = new MessageQueue();
|
|
22
|
+
expect(q.peek(0)).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("peek(0) on non-empty queue returns head without mutating length or totalBytes", () => {
|
|
26
|
+
const q = new MessageQueue();
|
|
27
|
+
q.push(makeItem("first", "r1"));
|
|
28
|
+
q.push(makeItem("second", "r2"));
|
|
29
|
+
|
|
30
|
+
const lengthBefore = q.length;
|
|
31
|
+
const bytesBefore = q.totalBytes;
|
|
32
|
+
|
|
33
|
+
const head = q.peek(0);
|
|
34
|
+
expect(head).toBeDefined();
|
|
35
|
+
expect(head?.requestId).toBe("r1");
|
|
36
|
+
expect(head?.content).toBe("first");
|
|
37
|
+
|
|
38
|
+
expect(q.length).toBe(lengthBefore);
|
|
39
|
+
expect(q.totalBytes).toBe(bytesBefore);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("peek(2) returns the third item; peek(99) returns undefined", () => {
|
|
43
|
+
const q = new MessageQueue();
|
|
44
|
+
q.push(makeItem("a", "r1"));
|
|
45
|
+
q.push(makeItem("b", "r2"));
|
|
46
|
+
q.push(makeItem("c", "r3"));
|
|
47
|
+
|
|
48
|
+
expect(q.peek(2)?.requestId).toBe("r3");
|
|
49
|
+
expect(q.peek(99)).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("MessageQueue.shiftN", () => {
|
|
54
|
+
test("shiftN(0) returns [] and does not mutate", () => {
|
|
55
|
+
const q = new MessageQueue();
|
|
56
|
+
q.push(makeItem("a", "r1"));
|
|
57
|
+
q.push(makeItem("b", "r2"));
|
|
58
|
+
|
|
59
|
+
const lengthBefore = q.length;
|
|
60
|
+
const bytesBefore = q.totalBytes;
|
|
61
|
+
|
|
62
|
+
const popped = q.shiftN(0);
|
|
63
|
+
expect(popped).toEqual([]);
|
|
64
|
+
expect(q.length).toBe(lengthBefore);
|
|
65
|
+
expect(q.totalBytes).toBe(bytesBefore);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("shiftN(2) on a 3-item queue returns the first two in FIFO order; length becomes 1; totalBytes matches remaining", () => {
|
|
69
|
+
const q = new MessageQueue();
|
|
70
|
+
q.push(makeItem("a", "r1"));
|
|
71
|
+
q.push(makeItem("b", "r2"));
|
|
72
|
+
q.push(makeItem("c", "r3"));
|
|
73
|
+
|
|
74
|
+
// Capture the remaining-item bytes by draining a fresh queue with just "c".
|
|
75
|
+
const reference = new MessageQueue();
|
|
76
|
+
reference.push(makeItem("c", "r3"));
|
|
77
|
+
const expectedRemainingBytes = reference.totalBytes;
|
|
78
|
+
|
|
79
|
+
const popped = q.shiftN(2);
|
|
80
|
+
expect(popped.map((m) => m.requestId)).toEqual(["r1", "r2"]);
|
|
81
|
+
expect(q.length).toBe(1);
|
|
82
|
+
expect(q.totalBytes).toBe(expectedRemainingBytes);
|
|
83
|
+
expect(q.peek(0)?.requestId).toBe("r3");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("shiftN(99) on a 3-item queue returns all three; length becomes 0; totalBytes becomes 0", () => {
|
|
87
|
+
const q = new MessageQueue();
|
|
88
|
+
q.push(makeItem("a", "r1"));
|
|
89
|
+
q.push(makeItem("b", "r2"));
|
|
90
|
+
q.push(makeItem("c", "r3"));
|
|
91
|
+
|
|
92
|
+
const popped = q.shiftN(99);
|
|
93
|
+
expect(popped.map((m) => m.requestId)).toEqual(["r1", "r2", "r3"]);
|
|
94
|
+
expect(q.length).toBe(0);
|
|
95
|
+
expect(q.totalBytes).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("after shiftN, a subsequent push up to the budget still succeeds (byte accounting not drifted)", () => {
|
|
99
|
+
// Tight budget so we can verify the freed bytes are exactly correct.
|
|
100
|
+
// Each 500-char item ≈ 500*2 + 512 = 1512 bytes.
|
|
101
|
+
// Budget of 3000 fits two items (3024 > 3000 — actually only one, since 1512+1512=3024 exceeds).
|
|
102
|
+
// Use 4000: fits two (3024) but not three (4536).
|
|
103
|
+
const q = new MessageQueue(4_000);
|
|
104
|
+
expect(q.push(makeItem("a".repeat(500), "r1"))).toBe(true);
|
|
105
|
+
expect(q.push(makeItem("b".repeat(500), "r2"))).toBe(true);
|
|
106
|
+
expect(q.push(makeItem("c".repeat(500), "r3"))).toBe(false);
|
|
107
|
+
|
|
108
|
+
// Drain both — budget should be fully reclaimed.
|
|
109
|
+
const popped = q.shiftN(2);
|
|
110
|
+
expect(popped).toHaveLength(2);
|
|
111
|
+
expect(q.totalBytes).toBe(0);
|
|
112
|
+
|
|
113
|
+
// Now we should be able to push two fresh items again.
|
|
114
|
+
expect(q.push(makeItem("d".repeat(500), "r4"))).toBe(true);
|
|
115
|
+
expect(q.push(makeItem("e".repeat(500), "r5"))).toBe(true);
|
|
116
|
+
expect(q.length).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("MessageQueue exports", () => {
|
|
121
|
+
test("DEFAULT_MAX_QUEUE_BYTES is importable and positive", () => {
|
|
122
|
+
expect(typeof DEFAULT_MAX_QUEUE_BYTES).toBe("number");
|
|
123
|
+
expect(DEFAULT_MAX_QUEUE_BYTES).toBeGreaterThan(0);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -508,7 +508,7 @@ describe("route policy registration", () => {
|
|
|
508
508
|
const policy = getPolicy("migrations/validate");
|
|
509
509
|
|
|
510
510
|
expect(policy).toBeDefined();
|
|
511
|
-
expect(policy?.requiredScopes).toContain("settings.
|
|
511
|
+
expect(policy?.requiredScopes).toContain("settings.read");
|
|
512
512
|
});
|
|
513
513
|
});
|
|
514
514
|
|
|
@@ -531,15 +531,15 @@ describe("auth policy shape", () => {
|
|
|
531
531
|
expect(policy!.allowedPrincipalTypes).toHaveLength(4);
|
|
532
532
|
});
|
|
533
533
|
|
|
534
|
-
test("export policy
|
|
534
|
+
test("export policy differs from validate policy on scopes (validate is read-only)", async () => {
|
|
535
535
|
const { getPolicy } = await import("../runtime/auth/route-policy.js");
|
|
536
536
|
const exportPolicy = getPolicy("migrations/export");
|
|
537
537
|
const validatePolicy = getPolicy("migrations/validate");
|
|
538
538
|
|
|
539
|
-
//
|
|
540
|
-
expect(exportPolicy!.requiredScopes).toEqual(
|
|
541
|
-
|
|
542
|
-
|
|
539
|
+
// validate is read-only so requires settings.read; export requires settings.write
|
|
540
|
+
expect(exportPolicy!.requiredScopes).toEqual(["settings.write"]);
|
|
541
|
+
expect(validatePolicy!.requiredScopes).toEqual(["settings.read"]);
|
|
542
|
+
// Both share the same principal types
|
|
543
543
|
expect(exportPolicy!.allowedPrincipalTypes).toEqual(
|
|
544
544
|
validatePolicy!.allowedPrincipalTypes,
|
|
545
545
|
);
|
|
@@ -988,23 +988,25 @@ describe("route policy registration", () => {
|
|
|
988
988
|
expect(policy?.allowedPrincipalTypes).toContain("local");
|
|
989
989
|
});
|
|
990
990
|
|
|
991
|
-
test("import policy matches
|
|
991
|
+
test("import policy matches export/preflight but differs from validate on scopes", async () => {
|
|
992
992
|
const { getPolicy } = await import("../runtime/auth/route-policy.js");
|
|
993
993
|
const importPolicy = getPolicy("migrations/import");
|
|
994
994
|
const validatePolicy = getPolicy("migrations/validate");
|
|
995
995
|
const exportPolicy = getPolicy("migrations/export");
|
|
996
996
|
const preflightPolicy = getPolicy("migrations/import-preflight");
|
|
997
997
|
|
|
998
|
+
// import, export, and preflight all require settings.write
|
|
999
|
+
expect(importPolicy!.requiredScopes).toEqual(exportPolicy!.requiredScopes);
|
|
998
1000
|
expect(importPolicy!.requiredScopes).toEqual(
|
|
999
|
-
|
|
1001
|
+
preflightPolicy!.requiredScopes,
|
|
1000
1002
|
);
|
|
1003
|
+
expect(importPolicy!.requiredScopes).toEqual(["settings.write"]);
|
|
1004
|
+
// validate is read-only so requires settings.read
|
|
1005
|
+
expect(validatePolicy!.requiredScopes).toEqual(["settings.read"]);
|
|
1006
|
+
// all share the same principal types
|
|
1001
1007
|
expect(importPolicy!.allowedPrincipalTypes).toEqual(
|
|
1002
1008
|
validatePolicy!.allowedPrincipalTypes,
|
|
1003
1009
|
);
|
|
1004
|
-
expect(importPolicy!.requiredScopes).toEqual(exportPolicy!.requiredScopes);
|
|
1005
|
-
expect(importPolicy!.requiredScopes).toEqual(
|
|
1006
|
-
preflightPolicy!.requiredScopes,
|
|
1007
|
-
);
|
|
1008
1010
|
});
|
|
1009
1011
|
});
|
|
1010
1012
|
|
|
@@ -720,21 +720,22 @@ describe("route policy registration", () => {
|
|
|
720
720
|
expect(policy?.allowedPrincipalTypes).toContain("local");
|
|
721
721
|
});
|
|
722
722
|
|
|
723
|
-
test("import-preflight policy matches
|
|
723
|
+
test("import-preflight policy matches export but differs from validate on scopes", async () => {
|
|
724
724
|
const { getPolicy } = await import("../runtime/auth/route-policy.js");
|
|
725
725
|
const preflightPolicy = getPolicy("migrations/import-preflight");
|
|
726
726
|
const validatePolicy = getPolicy("migrations/validate");
|
|
727
727
|
const exportPolicy = getPolicy("migrations/export");
|
|
728
728
|
|
|
729
|
+
// preflight and export both require settings.write
|
|
729
730
|
expect(preflightPolicy!.requiredScopes).toEqual(
|
|
730
|
-
|
|
731
|
+
exportPolicy!.requiredScopes,
|
|
731
732
|
);
|
|
733
|
+
// validate is read-only so requires settings.read
|
|
734
|
+
expect(validatePolicy!.requiredScopes).toEqual(["settings.read"]);
|
|
735
|
+
// all share the same principal types
|
|
732
736
|
expect(preflightPolicy!.allowedPrincipalTypes).toEqual(
|
|
733
737
|
validatePolicy!.allowedPrincipalTypes,
|
|
734
738
|
);
|
|
735
|
-
expect(preflightPolicy!.requiredScopes).toEqual(
|
|
736
|
-
exportPolicy!.requiredScopes,
|
|
737
|
-
);
|
|
738
739
|
});
|
|
739
740
|
});
|
|
740
741
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Success: valid .vbundle archive returns is_valid: true with manifest
|
|
6
6
|
* - Validation failures: invalid gzip, missing entries, bad manifest, checksum mismatches
|
|
7
7
|
* - Request errors: empty body, invalid multipart
|
|
8
|
-
* - Auth: route policy enforcement (settings.
|
|
8
|
+
* - Auth: route policy enforcement (settings.read scope required)
|
|
9
9
|
* - Integration: existing routes are unaffected by the new endpoint
|
|
10
10
|
*/
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
@@ -638,13 +638,13 @@ describe("handleMigrationValidate", () => {
|
|
|
638
638
|
// ---------------------------------------------------------------------------
|
|
639
639
|
|
|
640
640
|
describe("route policy registration", () => {
|
|
641
|
-
test("migrations/validate policy requires settings.
|
|
641
|
+
test("migrations/validate policy requires settings.read scope", async () => {
|
|
642
642
|
// Import route-policy to verify the registration exists
|
|
643
643
|
const { getPolicy } = await import("../runtime/auth/route-policy.js");
|
|
644
644
|
const policy = getPolicy("migrations/validate");
|
|
645
645
|
|
|
646
646
|
expect(policy).toBeDefined();
|
|
647
|
-
expect(policy?.requiredScopes).toContain("settings.
|
|
647
|
+
expect(policy?.requiredScopes).toContain("settings.read");
|
|
648
648
|
expect(policy?.allowedPrincipalTypes).toContain("actor");
|
|
649
649
|
expect(policy?.allowedPrincipalTypes).toContain("svc_gateway");
|
|
650
650
|
expect(policy?.allowedPrincipalTypes).toContain("local");
|